expo-modules-core 0.10.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +3 -3
  3. package/android/CMakeLists.txt +14 -5
  4. package/android/build.gradle +78 -28
  5. package/android/src/main/cpp/CachedReferencesRegistry.cpp +67 -0
  6. package/android/src/main/cpp/CachedReferencesRegistry.h +80 -0
  7. package/android/src/main/cpp/JNIFunctionBody.cpp +28 -12
  8. package/android/src/main/cpp/JNIFunctionBody.h +2 -2
  9. package/android/src/main/cpp/JNIInjector.cpp +4 -0
  10. package/android/src/main/cpp/JavaScriptModuleObject.cpp +86 -5
  11. package/android/src/main/cpp/JavaScriptModuleObject.h +27 -5
  12. package/android/src/main/cpp/JavaScriptRuntime.cpp +10 -12
  13. package/android/src/main/cpp/MethodMetadata.cpp +181 -40
  14. package/android/src/main/cpp/MethodMetadata.h +43 -3
  15. package/android/src/main/java/expo/modules/kotlin/AppContext.kt +63 -10
  16. package/android/src/main/java/expo/modules/kotlin/ModuleHolder.kt +6 -0
  17. package/android/src/main/java/expo/modules/kotlin/activityaware/AppCompatActivityAware.kt +49 -0
  18. package/android/src/main/java/expo/modules/kotlin/activityaware/AppCompatActivityAwareHelper.kt +43 -0
  19. package/android/src/main/java/expo/modules/kotlin/activityaware/OnActivityAvailableListener.kt +18 -0
  20. package/android/src/main/java/expo/modules/kotlin/activityresult/ActivityResultsManager.kt +99 -0
  21. package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultCaller.kt +25 -0
  22. package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultContract.kt +27 -0
  23. package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultFallbackCallback.kt +17 -0
  24. package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt +30 -0
  25. package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt +358 -0
  26. package/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt +135 -0
  27. package/android/src/main/java/expo/modules/kotlin/functions/AnyFunction.kt +34 -1
  28. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunction.kt +7 -1
  29. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionBuilder.kt +0 -108
  30. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionComponent.kt +5 -2
  31. package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionWithPromiseComponent.kt +5 -2
  32. package/android/src/main/java/expo/modules/kotlin/functions/SuspendFunctionComponent.kt +9 -2
  33. package/android/src/main/java/expo/modules/kotlin/functions/SyncFunctionComponent.kt +9 -1
  34. package/android/src/main/java/expo/modules/kotlin/jni/CppType.kt +1 -0
  35. package/android/src/main/java/expo/modules/kotlin/jni/JNIFunctionBody.kt +2 -2
  36. package/android/src/main/java/expo/modules/kotlin/jni/JavaScriptModuleObject.kt +4 -2
  37. package/android/src/main/java/expo/modules/kotlin/modules/Module.kt +1 -1
  38. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionBuilder.kt +5 -454
  39. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionData.kt +7 -15
  40. package/android/src/main/java/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +271 -0
  41. package/android/src/main/java/expo/modules/kotlin/objects/ObjectDefinitionData.kt +21 -0
  42. package/android/src/main/java/expo/modules/kotlin/objects/PropertyComponent.kt +54 -0
  43. package/android/src/main/java/expo/modules/kotlin/objects/PropertyComponentBuilder.kt +32 -0
  44. package/android/src/main/java/expo/modules/kotlin/types/AnyTypeConverter.kt +36 -0
  45. package/android/src/main/java/expo/modules/kotlin/types/TypeConverterProvider.kt +7 -0
  46. package/android/src/main/java/expo/modules/kotlin/views/ViewGroupDefinitionBuilder.kt +0 -41
  47. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinitionBuilder.kt +0 -33
  48. package/build/PermissionsInterface.d.ts +29 -0
  49. package/build/PermissionsInterface.d.ts.map +1 -1
  50. package/build/PermissionsInterface.js +9 -0
  51. package/build/PermissionsInterface.js.map +1 -1
  52. package/ios/ExpoModulesCore.podspec +2 -1
  53. package/ios/JSI/EXJSIInstaller.mm +2 -0
  54. package/ios/JSI/EXJSIUtils.h +1 -0
  55. package/ios/NativeModulesProxy/EXNativeModulesProxy.mm +4 -3
  56. package/ios/Swift/AppContext.swift +2 -4
  57. package/ios/Swift/Classes/ClassComponentElementsBuilder.swift +2 -2
  58. package/ios/Swift/Exceptions/ChainableException.swift +3 -3
  59. package/ios/Swift/ExpoBridgeModule.swift +16 -2
  60. package/ios/Swift/Logging/Logger.swift +3 -0
  61. package/ios/Swift/Promise.swift +5 -1
  62. package/package.json +2 -2
  63. package/src/PermissionsInterface.ts +29 -0
@@ -0,0 +1,358 @@
1
+ package expo.modules.kotlin.activityresult
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentSender
8
+ import android.os.Bundle
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import android.util.Log
12
+ import androidx.activity.ComponentActivity
13
+ import androidx.activity.result.ActivityResult
14
+ import androidx.activity.result.ActivityResultCallback
15
+ import androidx.activity.result.IntentSenderRequest
16
+ import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
17
+ import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
18
+ import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
19
+ import androidx.annotation.MainThread
20
+ import androidx.appcompat.app.AppCompatActivity
21
+ import androidx.core.app.ActivityCompat
22
+ import androidx.lifecycle.Lifecycle
23
+ import androidx.lifecycle.LifecycleEventObserver
24
+ import androidx.lifecycle.LifecycleOwner
25
+ import expo.modules.core.utilities.ifNull
26
+ import expo.modules.kotlin.AppContext
27
+ import expo.modules.kotlin.providers.CurrentActivityProvider
28
+ import java.io.Serializable
29
+ import java.util.*
30
+ import kotlin.collections.ArrayList
31
+ import kotlin.collections.HashMap
32
+
33
+ /**
34
+ * A registry that stores activity result callbacks ([ActivityResultCallback]) for
35
+ * [AppContextActivityResultCaller.registerForActivityResult] registered calls.
36
+ *
37
+ * This class is created to address the problems of integrating original [androidx.activity.result.ActivityResultRegistry]
38
+ * with ReactNative and our current architecture ([AppContext]).
39
+ * There are two main problems that this class is solving:
40
+ * - react-native-screen prevents us from using [Activity.onSaveInstanceState] / [Activity.onCreate] with `saveInstanceState`, because of https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
41
+ * - this might be fixable in react-native-screens itself
42
+ * - ReactNative does not provide any straightforward way to hook into every [Activity] / [Lifecycle] event that the original [androidx.activity.result.ActivityResultRegistry] mechanism depends on
43
+ * - there's room for further research in this case
44
+ *
45
+ * Ideally we would get rid of this class in favour of the original one, but firstly we need to
46
+ * solve these problems listed above.
47
+ *
48
+ * The implementation is based on [androidx.activity.result.ActivityResultRegistry] coming from `androidx.activity:activity:1.4.0` and `androidx.activity:activity-ktx:1.4.0`.
49
+ * Main differences are:
50
+ * - it operates on two callbacks instead of one
51
+ * - fallback callback - the secondary callback that is registered at the very beginning of the registry lifecycle (at the very beginning of the app's lifecycle).
52
+ * It is not aware of the context and serves to preserve the results coming from 3rd party Activity when Android kills the launching Activity.
53
+ * Additionally there's a supporting field that is serialized and deserialized that might hold some additional info about the result (like further instructions what to do about the result)
54
+ * - main callback - regular callback that allows single path execution of the asynchronous 3rd party Activity calls
55
+ * - it preserves the state across [Activity] recreation in different way - we use [android.content.SharedPreferences]
56
+ * - it is adjusted to work with [AppContext] and the lifecycle events ReactNative provides.
57
+ *
58
+ * @see [androidx.activity.result.ActivityResultRegistry] for more information.
59
+ */
60
+ class AppContextActivityResultRegistry(
61
+ private val currentActivityProvider: CurrentActivityProvider
62
+ ) {
63
+ private val LOG_TAG = "ActivityResultRegistry"
64
+
65
+ // Use upper 16 bits for request codes
66
+ private val INITIAL_REQUEST_CODE_VALUE = 0x00010000
67
+ private var random: Random = Random()
68
+
69
+ private val requestCodeToKey: MutableMap<Int, String> = HashMap()
70
+ private val keyToRequestCode: MutableMap<String, Int> = HashMap()
71
+ private val keyToLifecycleContainers: MutableMap<String, LifecycleContainer> = HashMap()
72
+ private var launchedKeys: ArrayList<String> = ArrayList()
73
+
74
+ /**
75
+ * Registry storing both main callbacks and fallback callbacks and contracts associated with key.
76
+ */
77
+ private val keyToCallbacksAndContract: MutableMap<String, CallbacksAndContract<*, *>> = HashMap()
78
+
79
+ /**
80
+ * A register that stores contract-specific parameters that allow proper resumption of the process
81
+ * in case of launching Activity being is destroyed.
82
+ * These are serialized and deserialized.
83
+ */
84
+ private val keyToInputParam: MutableMap<String, Serializable> = HashMap()
85
+
86
+ private val pendingResults = Bundle/*<String, ActivityResult>*/()
87
+
88
+ private val activity: AppCompatActivity
89
+ get() = requireNotNull(currentActivityProvider.currentActivity) { "Current Activity is not available at the moment" }
90
+
91
+ /**
92
+ * This method body is adapted mainly from [ComponentActivity.mActivityResultRegistry]
93
+ *
94
+ * @see [androidx.activity.result.ActivityResultRegistry.onLaunch]
95
+ */
96
+ @MainThread
97
+ fun <I : Serializable, O> onLaunch(
98
+ requestCode: Int,
99
+ contract: AppContextActivityResultContract<I, O>,
100
+ @SuppressLint("UnknownNullness") input: I,
101
+ ) {
102
+ // Start activity path
103
+ val intent = contract.createIntent(activity, input)
104
+ var optionsBundle: Bundle? = null
105
+
106
+ if (intent.hasExtra(StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE)) {
107
+ optionsBundle = intent.getBundleExtra(StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE)
108
+ intent.removeExtra(StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE)
109
+ }
110
+ when (intent.action) {
111
+ RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS -> {
112
+ // requestPermissions path
113
+ val permissions = intent
114
+ .getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS)
115
+ ?: arrayOfNulls(0)
116
+ ActivityCompat.requestPermissions(activity, permissions, requestCode)
117
+ }
118
+ StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST -> {
119
+ val request: IntentSenderRequest = intent.getParcelableExtra(StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST)!!
120
+ try {
121
+ // startIntentSenderForResult path
122
+ ActivityCompat.startIntentSenderForResult(
123
+ activity, request.intentSender,
124
+ requestCode, request.fillInIntent, request.flagsMask,
125
+ request.flagsValues, 0, optionsBundle
126
+ )
127
+ } catch (e: IntentSender.SendIntentException) {
128
+ Handler(Looper.getMainLooper()).post {
129
+ dispatchResult(
130
+ requestCode, Activity.RESULT_CANCELED,
131
+ Intent().setAction(StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST)
132
+ .putExtra(StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION, e)
133
+ )
134
+ }
135
+ }
136
+ }
137
+ else -> {
138
+ // startActivityForResult path
139
+ ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle)
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * This method should be called every time the Activity is created
146
+ *
147
+ * @param fallbackCallback callback that is invoked only if the Activity is destroyed and
148
+ * recreated by the Android OS. Regular results are returned using main callback coming from
149
+ * [AppContextActivityResultLauncher.launch] method.
150
+ *
151
+ * @see [androidx.activity.result.ActivityResultRegistry.register]
152
+ */
153
+ @MainThread
154
+ fun <I : Serializable, O> register(
155
+ key: String,
156
+ lifecycleOwner: LifecycleOwner,
157
+ contract: AppContextActivityResultContract<I, O>,
158
+ fallbackCallback: AppContextActivityResultFallbackCallback<I, O>
159
+ ): AppContextActivityResultLauncher<I, O> {
160
+ val lifecycle = lifecycleOwner.lifecycle
161
+
162
+ keyToCallbacksAndContract[key] = CallbacksAndContract(fallbackCallback, null, contract)
163
+ keyToRequestCode[key].ifNull {
164
+ val requestCode = generateRandomNumber()
165
+ requestCodeToKey[requestCode] = key
166
+ keyToRequestCode[key] = requestCode
167
+ }
168
+
169
+ val observer = LifecycleEventObserver { _, event ->
170
+ when (event) {
171
+ Lifecycle.Event.ON_START -> {
172
+ // This is the most common path for returning results
173
+ // When the Activity is destroyed then the other path is invoked, see [keyToFallbackCallback]
174
+
175
+ // 1. No callbacks registered yet, other path would take care of the results
176
+ @Suppress("UNCHECKED_CAST")
177
+ val callbacksAndContract: CallbacksAndContract<I, O> = (keyToCallbacksAndContract[key] ?: return@LifecycleEventObserver) as CallbacksAndContract<I, O>
178
+
179
+ // 2. There are results to be delivered to the callbacks
180
+ pendingResults.getParcelable<ActivityResult>(key)?.let {
181
+ pendingResults.remove(key)
182
+
183
+ @Suppress("UNCHECKED_CAST")
184
+ val input: I = keyToInputParam[key] as I
185
+ val result = callbacksAndContract.contract.parseResult(input, it.resultCode, it.data)
186
+
187
+ if (callbacksAndContract.mainCallback != null) {
188
+ // 2.1 there's a main callback available, so launching Activity has not been killed during the process
189
+ callbacksAndContract.mainCallback.onActivityResult(result)
190
+ } else {
191
+ // 2.2 launching Activity killed during the process, proceed with fallback callback
192
+ callbacksAndContract.fallbackCallback.onActivityResult(input, result)
193
+ }
194
+ }
195
+ }
196
+ Lifecycle.Event.ON_DESTROY -> {
197
+ unregister(key)
198
+ }
199
+ else -> Unit
200
+ }
201
+ }
202
+
203
+ val lifecycleContainer = keyToLifecycleContainers[key] ?: LifecycleContainer(lifecycle)
204
+ lifecycleContainer.addObserver(observer)
205
+ keyToLifecycleContainers[key] = lifecycleContainer
206
+
207
+ return object : AppContextActivityResultLauncher<I, O>() {
208
+ override fun launch(input: I, callback: ActivityResultCallback<O>) {
209
+ val requestCode = keyToRequestCode[key]
210
+ ?: throw IllegalStateException("Attempting to launch an unregistered ActivityResultLauncher with contract $contract and input $input. You must ensure the ActivityResultLauncher is registered before calling launch()")
211
+
212
+ @Suppress("UNCHECKED_CAST")
213
+ keyToCallbacksAndContract[key] = CallbacksAndContract(fallbackCallback, callback, contract)
214
+ keyToInputParam[key] = input
215
+ launchedKeys.add(key)
216
+
217
+ try {
218
+ onLaunch(requestCode, contract, input)
219
+ } catch (e: Exception) {
220
+ launchedKeys.remove(key)
221
+ throw e
222
+ }
223
+ }
224
+
225
+ override val contract = contract
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Persist the state of the registry.
231
+ */
232
+ fun persistInstanceState(context: Context) {
233
+ DataPersistor(context)
234
+ .addStringArrayList("launchedKeys", launchedKeys)
235
+ .addStringToIntMap("keyToRequestCode", keyToRequestCode)
236
+ .addStringToSerializableMap("keyToParamsForFallbackCallback", keyToInputParam.filter { (key) -> launchedKeys.contains(key) })
237
+ .addBundle("pendingResult", pendingResults)
238
+ .addSerializable("random", random)
239
+ .persist()
240
+ }
241
+
242
+ /**
243
+ * Possibly restore saved results from before the registry was destroyed.
244
+ */
245
+ fun restoreInstanceState(context: Context) {
246
+ val dataPersistor = DataPersistor(context)
247
+
248
+ dataPersistor.retrieveStringArrayList("launchedKeys")?.let { launchedKeys = it }
249
+ dataPersistor.retrieveStringToSerializableMap("keyToParamsForFallbackCallback")?.let { keyToInputParam.putAll(it) }
250
+ dataPersistor.retrieveBundle("pendingResult")?.let { pendingResults.putAll(it) }
251
+ dataPersistor.retrieveSerializable("random")?.let { random = it as Random }
252
+ dataPersistor.retrieveStringToIntMap("keyToRequestCode")?.let {
253
+ it.entries.forEach { (key, requestCode) ->
254
+ keyToRequestCode[key] = requestCode
255
+ requestCodeToKey[requestCode] = key
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @see [androidx.activity.result.ActivityResultRegistry.unregister]
262
+ */
263
+ @MainThread
264
+ fun unregister(key: String) {
265
+ if (!launchedKeys.contains(key)) {
266
+ // Only remove the key -> requestCode mapping if there isn't a launch in flight
267
+ keyToRequestCode.remove(key)?.let { requestCodeToKey.remove(it) }
268
+ }
269
+ keyToCallbacksAndContract.remove(key)
270
+ if (pendingResults.containsKey(key)) {
271
+ Log.w(LOG_TAG, "Dropping pending result for request $key : ${pendingResults.getParcelable<ActivityResult>(key)}")
272
+ pendingResults.remove(key)
273
+ }
274
+ keyToLifecycleContainers[key]?.let {
275
+ it.clearObservers()
276
+ keyToLifecycleContainers.remove(key)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Entry point for informing about data coming from [Activity.onActivityResult].
282
+ *
283
+ * @see [androidx.activity.result.ActivityResultRegistry.dispatchResult]
284
+ */
285
+ @MainThread
286
+ fun dispatchResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
287
+ val key = requestCodeToKey[requestCode] ?: return false
288
+ val callbacksAndContract = keyToCallbacksAndContract[key]
289
+ doDispatch(key, resultCode, data, callbacksAndContract)
290
+ return true
291
+ }
292
+
293
+ /**
294
+ * This method has three different flows:
295
+ * 1. main callback available (launcher Activity has not been killed), so resume main flow with results
296
+ * 2. launcher Activity has been recreated and it has already proceeded to [Lifecycle.State.STARTED] phase, so use fallback callback
297
+ * 3. results are delivered, but [Activity] has not yet reached [Lifecycle.State.STARTED] phase, so save them got later use
298
+ */
299
+ private fun <I : Serializable, O> doDispatch(
300
+ key: String,
301
+ resultCode: Int,
302
+ data: Intent?,
303
+ callbacksAndContract: CallbacksAndContract<I, O>?
304
+ ) {
305
+ val currentLifecycleState = keyToLifecycleContainers[key]?.lifecycle?.currentState
306
+
307
+ if (callbacksAndContract?.mainCallback != null && launchedKeys.contains(key)) {
308
+ // 1. There's main callback available, so use it right away
309
+ @Suppress("UNCHECKED_CAST")
310
+ val input = keyToInputParam[key] as I
311
+ callbacksAndContract.mainCallback.onActivityResult(callbacksAndContract.contract.parseResult(input, resultCode, data))
312
+ launchedKeys.remove(key)
313
+ } else if (currentLifecycleState != null && currentLifecycleState.isAtLeast(Lifecycle.State.STARTED) && callbacksAndContract != null && launchedKeys.contains(key)) {
314
+ // 2. Activity has already started, so let's proceed with fallback callback scenario
315
+ @Suppress("UNCHECKED_CAST")
316
+ val input = keyToInputParam[key] as I
317
+ callbacksAndContract.fallbackCallback.onActivityResult(input, callbacksAndContract.contract.parseResult(input, resultCode, data))
318
+ launchedKeys.remove(key)
319
+ } else {
320
+ // 3. Add these pending results in their place in order to wait for Lifecycle-based path
321
+ pendingResults.putParcelable(key, ActivityResult(resultCode, data))
322
+ }
323
+ }
324
+
325
+ private fun generateRandomNumber(): Int {
326
+ var number = (random.nextInt(Int.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE + 1) + INITIAL_REQUEST_CODE_VALUE)
327
+ while (requestCodeToKey.containsKey(number)) {
328
+ number = (random.nextInt(Int.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE + 1) + INITIAL_REQUEST_CODE_VALUE)
329
+ }
330
+ return number
331
+ }
332
+
333
+ private data class CallbacksAndContract<I : Serializable, O>(
334
+ /**
335
+ * Fallback callback that accepts both output and deserialized input parameters
336
+ */
337
+ val fallbackCallback: AppContextActivityResultFallbackCallback<I, O>,
338
+ /**
339
+ * Main callback that might not be available, because the app might be re-created
340
+ */
341
+ val mainCallback: ActivityResultCallback<O>?,
342
+ val contract: AppContextActivityResultContract<I, O>,
343
+ )
344
+
345
+ class LifecycleContainer internal constructor(val lifecycle: Lifecycle) {
346
+ private val observers: ArrayList<LifecycleEventObserver> = ArrayList()
347
+
348
+ fun addObserver(observer: LifecycleEventObserver) {
349
+ lifecycle.addObserver(observer)
350
+ observers.add(observer)
351
+ }
352
+
353
+ fun clearObservers() {
354
+ observers.forEach { lifecycle.removeObserver(it) }
355
+ observers.clear()
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,135 @@
1
+ package expo.modules.kotlin.activityresult
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.os.Bundle
6
+ import android.os.Parcel
7
+ import android.text.format.DateUtils
8
+ import android.util.Base64
9
+ import androidx.core.os.bundleOf
10
+ import java.io.Serializable
11
+ import java.util.*
12
+ import kotlin.collections.ArrayList
13
+
14
+ const val EXPIRE_KEY = "expire"
15
+ const val EXPIRATION_TIME = 5 * DateUtils.MINUTE_IN_MILLIS
16
+
17
+ /**
18
+ * This class serves as a persistable store that accepts different kinds of data that have to
19
+ * be preserved between Activity destruction and recreation.
20
+ * For each kind of data there's a separate pair of methods for storing and retrieving the data.
21
+ *
22
+ * Ideally we would use [android.app.Activity.onSaveInstanceState] and [android.app.Activity.onCreate]
23
+ * alongside with `savedBundleState`, but it's blocked by https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
24
+ */
25
+ class DataPersistor(context: Context) {
26
+ private val sharedPreferences: SharedPreferences = context.getSharedPreferences("expo.modules.kotlin.PersistentDataManager", Context.MODE_PRIVATE)
27
+
28
+ private val accumulator = Bundle()
29
+ private val retrievedData by lazy { retrieveData() }
30
+
31
+ fun addStringArrayList(key: String, value: ArrayList<String>) = apply {
32
+ accumulator.putStringArrayList(key, value)
33
+ }
34
+
35
+ fun retrieveStringArrayList(key: String): java.util.ArrayList<String>? {
36
+ return retrievedData.getStringArrayList(key)
37
+ }
38
+
39
+ fun addStringToIntMap(key: String, value: Map<String, Int>) = apply {
40
+ accumulator.putBundle(key, bundleOf(*value.toList().toTypedArray()))
41
+ }
42
+
43
+ fun retrieveStringToIntMap(key: String): Map<String, Int>? {
44
+ return retrievedData.getBundle(key)?.let { bundle ->
45
+ val keys = bundle.keySet()
46
+ keys.associateWith { bundle.getInt(it) }
47
+ }
48
+ }
49
+
50
+ fun addStringToSerializableMap(key: String, value: Map<String, Serializable>) = apply {
51
+ accumulator.putBundle(
52
+ key,
53
+ bundleOf(
54
+ *value
55
+ .toList()
56
+ .toTypedArray()
57
+ )
58
+ )
59
+ }
60
+
61
+ fun retrieveStringToSerializableMap(key: String): Map<String, Serializable>? {
62
+ return retrievedData.getBundle(key)
63
+ ?.let { bundle ->
64
+ bundle
65
+ .keySet()
66
+ .associateWith { key ->
67
+ bundle.getSerializable(key) ?: throw IllegalStateException("For a key '$key' there should be a serializable class available")
68
+ }
69
+ }
70
+ }
71
+
72
+ fun addBundle(key: String, value: Bundle) = apply {
73
+ accumulator.putBundle(key, value)
74
+ }
75
+
76
+ fun retrieveBundle(key: String): Bundle? {
77
+ return retrievedData.getBundle(key)
78
+ }
79
+
80
+ fun addSerializable(key: String, value: Serializable) = apply {
81
+ accumulator.putSerializable(key, value)
82
+ }
83
+
84
+ fun retrieveSerializable(key: String): Serializable? {
85
+ return retrievedData.getSerializable(key)
86
+ }
87
+
88
+ fun persist() {
89
+ val editor = sharedPreferences.edit()
90
+
91
+ editor.putString("bundle", accumulator.toBase64())
92
+ editor.putLong(EXPIRE_KEY, Date().time + EXPIRATION_TIME)
93
+
94
+ @Suppress("ApplySharedPref")
95
+ editor.commit()
96
+ }
97
+
98
+ private fun retrieveData(): Bundle {
99
+ var result = Bundle()
100
+ val expirationTime = sharedPreferences.getLong(EXPIRE_KEY, 0)
101
+ if (expirationTime > Date().time) {
102
+ val stringResult = sharedPreferences.getString("bundle", null)
103
+ result = stringResult?.toBundle() ?: result
104
+ }
105
+
106
+ sharedPreferences
107
+ .edit()
108
+ .clear()
109
+ .apply()
110
+
111
+ return result
112
+ }
113
+ }
114
+
115
+ private fun String.toBundle(): Bundle? {
116
+ val bytes = Base64.decode(this, 0)
117
+ return Parcel.obtain().run {
118
+ unmarshall(bytes, 0, bytes.size)
119
+ setDataPosition(0)
120
+ @Suppress("ParcelClassLoader")
121
+ val bundle = readBundle(null)
122
+ recycle()
123
+ bundle
124
+ }
125
+ }
126
+
127
+ private fun Bundle.toBase64(): String {
128
+ val bytes = Parcel.obtain().run {
129
+ writeBundle(this@toBase64)
130
+ val bytes = marshall()
131
+ recycle()
132
+ bytes
133
+ }
134
+ return Base64.encodeToString(bytes, 0)
135
+ }
@@ -1,6 +1,7 @@
1
1
  package expo.modules.kotlin.functions
2
2
 
3
3
  import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableType
4
5
  import expo.modules.kotlin.AppContext
5
6
  import expo.modules.kotlin.exception.ArgumentCastException
6
7
  import expo.modules.kotlin.exception.CodedException
@@ -16,7 +17,7 @@ import expo.modules.kotlin.types.AnyType
16
17
  */
17
18
  abstract class AnyFunction(
18
19
  protected val name: String,
19
- private val desiredArgsTypes: Array<AnyType>
20
+ protected val desiredArgsTypes: Array<AnyType>
20
21
  ) {
21
22
  internal val argsCount get() = desiredArgsTypes.size
22
23
 
@@ -48,8 +49,40 @@ abstract class AnyFunction(
48
49
  return finalArgs
49
50
  }
50
51
 
52
+ /**
53
+ * Tries to convert arguments from [Any]? to expected types.
54
+ *
55
+ * @return An array of converted arguments
56
+ * @throws `CodedException` if conversion isn't possible
57
+ */
58
+ @Throws(CodedException::class)
59
+ protected fun convertArgs(args: Array<Any?>): Array<out Any?> {
60
+ if (desiredArgsTypes.size != args.size) {
61
+ throw InvalidArgsNumberException(args.size, desiredArgsTypes.size)
62
+ }
63
+
64
+ val finalArgs = Array<Any?>(desiredArgsTypes.size) { null }
65
+ val argIterator = args.iterator()
66
+ desiredArgsTypes
67
+ .withIndex()
68
+ .forEach { (index, desiredType) ->
69
+ val element = argIterator.next()
70
+
71
+ exceptionDecorator({ cause ->
72
+ ArgumentCastException(desiredType.kType, index, ReadableType.String, cause)
73
+ }) {
74
+ finalArgs[index] = desiredType.convert(element)
75
+ }
76
+ }
77
+ return finalArgs
78
+ }
79
+
51
80
  /**
52
81
  * Attaches current function to the provided js object.
53
82
  */
54
83
  abstract fun attachToJSObject(appContext: AppContext, jsObject: JavaScriptModuleObject)
84
+
85
+ fun getCppRequiredTypes(): IntArray {
86
+ return desiredArgsTypes.map { it.getCppRequiredTypes() }.toIntArray()
87
+ }
55
88
  }
@@ -20,8 +20,14 @@ abstract class AsyncFunction(
20
20
  @Throws(CodedException::class)
21
21
  abstract fun call(args: ReadableArray, promise: Promise)
22
22
 
23
+ abstract fun call(args: Array<Any?>, promise: Promise)
24
+
23
25
  override fun attachToJSObject(appContext: AppContext, jsObject: JavaScriptModuleObject) {
24
- jsObject.registerAsyncFunction(name, argsCount) { args, bridgePromise ->
26
+ jsObject.registerAsyncFunction(
27
+ name,
28
+ argsCount,
29
+ desiredArgsTypes.map { it.getCppRequiredTypes() }.toIntArray()
30
+ ) { args, bridgePromise ->
25
31
  val kotlinPromise = KPromiseWrapper(bridgePromise as com.facebook.react.bridge.Promise)
26
32
  appContext.modulesQueue.launch {
27
33
  try {