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.
- package/CHANGELOG.md +23 -0
- package/README.md +3 -3
- package/android/CMakeLists.txt +14 -5
- package/android/build.gradle +78 -28
- package/android/src/main/cpp/CachedReferencesRegistry.cpp +67 -0
- package/android/src/main/cpp/CachedReferencesRegistry.h +80 -0
- package/android/src/main/cpp/JNIFunctionBody.cpp +28 -12
- package/android/src/main/cpp/JNIFunctionBody.h +2 -2
- package/android/src/main/cpp/JNIInjector.cpp +4 -0
- package/android/src/main/cpp/JavaScriptModuleObject.cpp +86 -5
- package/android/src/main/cpp/JavaScriptModuleObject.h +27 -5
- package/android/src/main/cpp/JavaScriptRuntime.cpp +10 -12
- package/android/src/main/cpp/MethodMetadata.cpp +181 -40
- package/android/src/main/cpp/MethodMetadata.h +43 -3
- package/android/src/main/java/expo/modules/kotlin/AppContext.kt +63 -10
- package/android/src/main/java/expo/modules/kotlin/ModuleHolder.kt +6 -0
- package/android/src/main/java/expo/modules/kotlin/activityaware/AppCompatActivityAware.kt +49 -0
- package/android/src/main/java/expo/modules/kotlin/activityaware/AppCompatActivityAwareHelper.kt +43 -0
- package/android/src/main/java/expo/modules/kotlin/activityaware/OnActivityAvailableListener.kt +18 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/ActivityResultsManager.kt +99 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultCaller.kt +25 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultContract.kt +27 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultFallbackCallback.kt +17 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt +30 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt +358 -0
- package/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt +135 -0
- package/android/src/main/java/expo/modules/kotlin/functions/AnyFunction.kt +34 -1
- package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunction.kt +7 -1
- package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionBuilder.kt +0 -108
- package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionComponent.kt +5 -2
- package/android/src/main/java/expo/modules/kotlin/functions/AsyncFunctionWithPromiseComponent.kt +5 -2
- package/android/src/main/java/expo/modules/kotlin/functions/SuspendFunctionComponent.kt +9 -2
- package/android/src/main/java/expo/modules/kotlin/functions/SyncFunctionComponent.kt +9 -1
- package/android/src/main/java/expo/modules/kotlin/jni/CppType.kt +1 -0
- package/android/src/main/java/expo/modules/kotlin/jni/JNIFunctionBody.kt +2 -2
- package/android/src/main/java/expo/modules/kotlin/jni/JavaScriptModuleObject.kt +4 -2
- package/android/src/main/java/expo/modules/kotlin/modules/Module.kt +1 -1
- package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionBuilder.kt +5 -454
- package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionData.kt +7 -15
- package/android/src/main/java/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +271 -0
- package/android/src/main/java/expo/modules/kotlin/objects/ObjectDefinitionData.kt +21 -0
- package/android/src/main/java/expo/modules/kotlin/objects/PropertyComponent.kt +54 -0
- package/android/src/main/java/expo/modules/kotlin/objects/PropertyComponentBuilder.kt +32 -0
- package/android/src/main/java/expo/modules/kotlin/types/AnyTypeConverter.kt +36 -0
- package/android/src/main/java/expo/modules/kotlin/types/TypeConverterProvider.kt +7 -0
- package/android/src/main/java/expo/modules/kotlin/views/ViewGroupDefinitionBuilder.kt +0 -41
- package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinitionBuilder.kt +0 -33
- package/build/PermissionsInterface.d.ts +29 -0
- package/build/PermissionsInterface.d.ts.map +1 -1
- package/build/PermissionsInterface.js +9 -0
- package/build/PermissionsInterface.js.map +1 -1
- package/ios/ExpoModulesCore.podspec +2 -1
- package/ios/JSI/EXJSIInstaller.mm +2 -0
- package/ios/JSI/EXJSIUtils.h +1 -0
- package/ios/NativeModulesProxy/EXNativeModulesProxy.mm +4 -3
- package/ios/Swift/AppContext.swift +2 -4
- package/ios/Swift/Classes/ClassComponentElementsBuilder.swift +2 -2
- package/ios/Swift/Exceptions/ChainableException.swift +3 -3
- package/ios/Swift/ExpoBridgeModule.swift +16 -2
- package/ios/Swift/Logging/Logger.swift +3 -0
- package/ios/Swift/Promise.swift +5 -1
- package/package.json +2 -2
- package/src/PermissionsInterface.ts +29 -0
package/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt
ADDED
|
@@ -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
|
-
|
|
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(
|
|
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 {
|