expo-modules-core 1.5.11 → 1.5.13

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 CHANGED
@@ -10,6 +10,21 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.5.13 — 2024-01-10
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fixed random `NullPointerExceptions` when calling `Updates.reloadAsync` on Android. ([#24442](https://github.com/expo/expo/pull/24442) by [@lukmccall](https://github.com/lukmccall))
18
+
19
+ ## 1.5.12 — 2023-11-17
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - [Android] Improve boot time on low-end devices. ([#25267](https://github.com/expo/expo/pull/25267) by [@lukmccall](https://github.com/lukmccall))
24
+ - [Android] Improve performance of enum and map converters. ([#25272](https://github.com/expo/expo/pull/25272) by [@lukmccall](https://github.com/lukmccall))
25
+ - [Android] Improve logic responsible for obtaining converters that slow down the startup time. ([#25273](https://github.com/expo/expo/pull/25273) by [@lukmccall](https://github.com/lukmccall))
26
+ - [Android] Improving the creation process of views for better performance. ([#25274](https://github.com/expo/expo/pull/25274) by [@lukmccall](https://github.com/lukmccall))
27
+
13
28
  ## 1.5.11 — 2023-08-29
14
29
 
15
30
  ### 🐛 Bug fixes
@@ -6,7 +6,7 @@ apply plugin: 'maven-publish'
6
6
  apply plugin: "de.undercouch.download"
7
7
 
8
8
  group = 'host.exp.exponent'
9
- version = '1.5.11'
9
+ version = '1.5.13'
10
10
 
11
11
  buildscript {
12
12
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
@@ -160,7 +160,7 @@ android {
160
160
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
161
161
  consumerProguardFiles 'proguard-rules.pro'
162
162
  versionCode 1
163
- versionName "1.5.11"
163
+ versionName "1.5.13"
164
164
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
165
165
 
166
166
  testInstrumentationRunner "expo.modules.TestRunner"
@@ -1,5 +1,6 @@
1
1
  package expo.modules.kotlin
2
2
 
3
+ import android.view.View
3
4
  import expo.modules.kotlin.events.EventName
4
5
  import expo.modules.kotlin.modules.Module
5
6
  import expo.modules.kotlin.tracing.trace
@@ -7,9 +8,7 @@ import kotlinx.coroutines.CoroutineName
7
8
  import kotlinx.coroutines.CoroutineScope
8
9
  import kotlinx.coroutines.Dispatchers
9
10
  import kotlinx.coroutines.SupervisorJob
10
- import kotlinx.coroutines.launch
11
11
  import java.lang.ref.WeakReference
12
- import kotlin.reflect.full.declaredMemberProperties
13
12
 
14
13
  class ModuleRegistry(
15
14
  private val appContext: WeakReference<AppContext>
@@ -33,18 +32,6 @@ class ModuleRegistry(
33
32
  holder.apply {
34
33
  post(EventName.MODULE_CREATE)
35
34
  registerContracts()
36
-
37
- // The initial invocation of `declaredMemberProperties` appears to be slow,
38
- // as Kotlin must deserialize metadata internally.
39
- // This is a known issue that may be resolved by the new K2 compiler in the future.
40
- // However, until then, we must find a way to address this problem.
41
- // Therefore, we have decided to dispatch a lambda
42
- // that invokes `declaredMemberProperties` during module creation.
43
- viewClass()?.let { viewType ->
44
- appContext.get()?.backgroundCoroutineScope?.launch {
45
- viewType.declaredMemberProperties
46
- }
47
- }
48
35
  }
49
36
 
50
37
  registry[holder.name] = holder
@@ -70,6 +57,16 @@ class ModuleRegistry(
70
57
  fun getModuleHolder(module: Module): ModuleHolder? =
71
58
  registry.values.find { it.module === module }
72
59
 
60
+ fun <T : View> getModuleHolder(viewClass: Class<T>): ModuleHolder? {
61
+ return registry.firstNotNullOfOrNull { (_, holder) ->
62
+ if (holder.definition.viewManagerDefinition?.viewType == viewClass) {
63
+ holder
64
+ } else {
65
+ null
66
+ }
67
+ }
68
+ }
69
+
73
70
  fun post(eventName: EventName) {
74
71
  forEach {
75
72
  it.post(eventName)
@@ -5,24 +5,36 @@ import com.facebook.react.bridge.WritableNativeArray
5
5
  import com.facebook.react.bridge.WritableNativeMap
6
6
  import expo.modules.core.interfaces.DoNotStrip
7
7
  import expo.modules.kotlin.exception.UnexpectedException
8
+ import expo.modules.kotlin.logger
8
9
 
9
10
  @Suppress("KotlinJniMissingFunction")
10
11
  @DoNotStrip
11
12
  class JavaCallback @DoNotStrip internal constructor(@DoNotStrip private val mHybridData: HybridData) : Destructible {
12
13
  operator fun invoke(result: Any?) {
13
- if (result == null) {
14
- invoke()
15
- return
16
- }
17
- when (result) {
18
- is Int -> invoke(result)
19
- is Boolean -> invoke(result)
20
- is Double -> invoke(result)
21
- is Float -> invoke(result)
22
- is String -> invoke(result)
23
- is WritableNativeArray -> invoke(result)
24
- is WritableNativeMap -> invoke(result)
25
- else -> throw UnexpectedException("Unknown type: ${result.javaClass}")
14
+ try {
15
+
16
+ if (result == null) {
17
+ invoke()
18
+ return
19
+ }
20
+ when (result) {
21
+ is Int -> invoke(result)
22
+ is Boolean -> invoke(result)
23
+ is Double -> invoke(result)
24
+ is Float -> invoke(result)
25
+ is String -> invoke(result)
26
+ is WritableNativeArray -> invoke(result)
27
+ is WritableNativeMap -> invoke(result)
28
+ else -> throw UnexpectedException("Unknown type: ${result.javaClass}")
29
+ }
30
+ } catch (e: Throwable) {
31
+ if (!mHybridData.isValid) {
32
+ // We know that this particular JavaCallback was invalidated, so it shouldn't be invoked.
33
+ // To prevent crashes, we decided to suppress the error here.
34
+ logger.error("Invalidated JavaCallback was invoked", e)
35
+ return
36
+ }
37
+ throw e
26
38
  }
27
39
  }
28
40
 
@@ -8,8 +8,6 @@ import expo.modules.kotlin.logger
8
8
  import expo.modules.kotlin.toKType
9
9
  import kotlin.reflect.KClass
10
10
  import kotlin.reflect.full.createType
11
- import kotlin.reflect.full.declaredMemberProperties
12
- import kotlin.reflect.full.isSubclassOf
13
11
  import kotlin.reflect.full.primaryConstructor
14
12
 
15
13
  class EnumTypeConverter(
@@ -29,7 +27,7 @@ class EnumTypeConverter(
29
27
  }
30
28
 
31
29
  init {
32
- if (!enumClass.isSubclassOf(Enumerable::class)) {
30
+ if (Enumerable::class.java.isAssignableFrom(enumClass.java)) {
33
31
  logger.warn("Enum '$enumClass' should inherit from ${Enumerable::class}.")
34
32
  }
35
33
  }
@@ -87,22 +85,20 @@ class EnumTypeConverter(
87
85
  enumConstants: Array<out Enum<*>>,
88
86
  parameterName: String
89
87
  ): Enum<*> {
90
- // To obtain the value of parameter, we have to find a property that is connected with this parameter.
91
- @Suppress("UNCHECKED_CAST")
92
- val parameterProperty = enumClass
93
- .declaredMemberProperties
94
- .find { it.name == parameterName }
95
- requireNotNull(parameterProperty) { "Cannot find a property for $parameterName parameter" }
88
+ val filed = enumClass.java.getDeclaredField(parameterName)
89
+ requireNotNull(filed) { "Cannot find a property for $parameterName parameter" }
96
90
 
97
- val parameterType = parameterProperty.returnType.classifier
91
+ filed.isAccessible = true
92
+
93
+ val parameterType = filed.type
98
94
  val jsUnwrapValue = if (jsValue is Dynamic) {
99
- if (parameterType == String::class) {
95
+ if (parameterType == String::class.java) {
100
96
  jsValue.asString()
101
97
  } else {
102
98
  jsValue.asInt()
103
99
  }
104
100
  } else {
105
- if (parameterType == String::class) {
101
+ if (parameterType == String::class.java) {
106
102
  jsValue as String
107
103
  } else {
108
104
  if (jsValue is Double) {
@@ -115,7 +111,7 @@ class EnumTypeConverter(
115
111
 
116
112
  return requireNotNull(
117
113
  enumConstants.find {
118
- parameterProperty.get(it) == jsUnwrapValue
114
+ filed.get(it) == jsUnwrapValue
119
115
  }
120
116
  ) { "Couldn't convert '$jsValue' to ${enumClass.simpleName} where $parameterName is the enum parameter" }
121
117
  }
@@ -10,14 +10,13 @@ import expo.modules.kotlin.exception.exceptionDecorator
10
10
  import expo.modules.kotlin.jni.ExpectedType
11
11
  import expo.modules.kotlin.recycle
12
12
  import kotlin.reflect.KType
13
- import kotlin.reflect.typeOf
14
13
 
15
14
  class MapTypeConverter(
16
15
  converterProvider: TypeConverterProvider,
17
16
  private val mapType: KType
18
17
  ) : DynamicAwareTypeConverters<Map<*, *>>(mapType.isMarkedNullable) {
19
18
  init {
20
- require(mapType.arguments.first().type == typeOf<String>()) {
19
+ require(mapType.arguments.first().type?.classifier == String::class) {
21
20
  "The map key type should be String, but received ${mapType.arguments.first()}."
22
21
  }
23
22
  }
@@ -44,7 +44,6 @@ import java.net.URL
44
44
  import java.nio.file.Path
45
45
  import kotlin.reflect.KClass
46
46
  import kotlin.reflect.KType
47
- import kotlin.reflect.full.isSubclassOf
48
47
  import kotlin.reflect.typeOf
49
48
 
50
49
  interface TypeConverterProvider {
@@ -92,28 +91,25 @@ object TypeConverterProviderImpl : TypeConverterProvider {
92
91
  }
93
92
 
94
93
  val kClass = type.classifier as? KClass<*> ?: throw MissingTypeConverter(type)
94
+ val jClass = kClass.java
95
95
 
96
- if (kClass.java.isArray) {
96
+ if (jClass.isArray || Array::class.java.isAssignableFrom(jClass)) {
97
97
  return ArrayTypeConverter(this, type)
98
98
  }
99
99
 
100
- if (kClass.isSubclassOf(List::class)) {
100
+ if (List::class.java.isAssignableFrom(jClass)) {
101
101
  return ListTypeConverter(this, type)
102
102
  }
103
103
 
104
- if (kClass.isSubclassOf(Map::class)) {
104
+ if (Map::class.java.isAssignableFrom(jClass)) {
105
105
  return MapTypeConverter(this, type)
106
106
  }
107
107
 
108
- if (kClass.isSubclassOf(Pair::class)) {
108
+ if (Pair::class.java.isAssignableFrom(jClass)) {
109
109
  return PairTypeConverter(this, type)
110
110
  }
111
111
 
112
- if (kClass.isSubclassOf(Array::class)) {
113
- return ArrayTypeConverter(this, type)
114
- }
115
-
116
- if (kClass.java.isEnum) {
112
+ if (jClass.isEnum) {
117
113
  @Suppress("UNCHECKED_CAST")
118
114
  return EnumTypeConverter(kClass as KClass<Enum<*>>, type.isMarkedNullable)
119
115
  }
@@ -123,36 +119,36 @@ object TypeConverterProviderImpl : TypeConverterProvider {
123
119
  return cachedConverter
124
120
  }
125
121
 
126
- if (kClass.isSubclassOf(Record::class)) {
122
+ if (Record::class.java.isAssignableFrom(jClass)) {
127
123
  val converter = RecordTypeConverter<Record>(this, type)
128
124
  cachedRecordConverters[kClass] = converter
129
125
  return converter
130
126
  }
131
127
 
132
- if (kClass.isSubclassOf(View::class)) {
128
+ if (View::class.java.isAssignableFrom(jClass)) {
133
129
  return ViewTypeConverter<View>(type)
134
130
  }
135
131
 
136
- if (kClass.isSubclassOf(SharedObject::class)) {
132
+ if (SharedObject::class.java.isAssignableFrom(jClass)) {
137
133
  return SharedObjectTypeConverter<SharedObject>(type)
138
134
  }
139
135
 
140
- if (kClass.isSubclassOf(JavaScriptFunction::class)) {
136
+ if (JavaScriptFunction::class.java.isAssignableFrom(jClass)) {
141
137
  return JavaScriptFunctionTypeConverter<Any>(type)
142
138
  }
143
139
 
144
- return handelEither(type, kClass)
140
+ return handelEither(type, jClass)
145
141
  ?: handelCustomConverter(type, kClass)
146
142
  ?: throw MissingTypeConverter(type)
147
143
  }
148
144
 
149
145
  @OptIn(EitherType::class)
150
- private fun handelEither(type: KType, kClass: KClass<*>): TypeConverter<*>? {
151
- if (kClass.isSubclassOf(Either::class)) {
152
- if (kClass.isSubclassOf(EitherOfFour::class)) {
146
+ private fun handelEither(type: KType, jClass: Class<*>): TypeConverter<*>? {
147
+ if (Either::class.java.isAssignableFrom(jClass)) {
148
+ if (EitherOfFour::class.java.isAssignableFrom(jClass)) {
153
149
  return EitherOfFourTypeConverter<Any, Any, Any, Any>(this, type)
154
150
  }
155
- if (kClass.isSubclassOf(EitherOfThree::class)) {
151
+ if (EitherOfThree::class.java.isAssignableFrom(jClass)) {
156
152
  return EitherOfThreeTypeConverter<Any, Any, Any>(this, type)
157
153
  }
158
154
  return EitherTypeConverter<Any, Any>(this, type)
@@ -4,22 +4,21 @@ import android.view.View
4
4
  import com.facebook.react.bridge.ReactContext
5
5
  import com.facebook.react.bridge.WritableMap
6
6
  import expo.modules.adapters.react.NativeModulesProxy
7
- import expo.modules.kotlin.modules.Module
7
+ import expo.modules.core.utilities.ifNull
8
+ import expo.modules.kotlin.logger
8
9
  import expo.modules.kotlin.types.JSTypeConverter
9
10
  import expo.modules.kotlin.types.putGeneric
10
- import kotlin.reflect.KType
11
11
 
12
12
  fun interface ViewEventCallback<T> {
13
13
  operator fun invoke(arg: T)
14
14
  }
15
15
 
16
- class ViewEvent<T>(
16
+ open class ViewEvent<T>(
17
17
  private val name: String,
18
- private val type: KType,
19
18
  private val view: View,
20
19
  private val coalescingKey: CoalescingKey<T>?
21
20
  ) : ViewEventCallback<T> {
22
- internal lateinit var module: Module
21
+ private var isValidated = false
23
22
 
24
23
  override operator fun invoke(arg: T) {
25
24
  val reactContext = view.context as ReactContext
@@ -29,6 +28,24 @@ class ViewEvent<T>(
29
28
  ?: return
30
29
  val appContext = nativeModulesProxy.kotlinInteropModuleRegistry.appContext
31
30
 
31
+ if (!isValidated) {
32
+ val holder = appContext.registry.getModuleHolder(view::class.java).ifNull {
33
+ logger.warn("⚠️ Cannot get module holder for ${view::class.java}")
34
+ return
35
+ }
36
+ val callbacks = holder.definition.viewManagerDefinition?.callbacksDefinition.ifNull {
37
+ logger.warn("⚠️ Cannot get callbacks for ${holder.module::class.java}")
38
+ return
39
+ }
40
+
41
+ if (!callbacks.names.any { it == name }) {
42
+ logger.warn("⚠️ Event $name wasn't exported from ${holder.module::class.java}")
43
+ return
44
+ }
45
+
46
+ isValidated = true
47
+ }
48
+
32
49
  appContext
33
50
  .callbackInvoker
34
51
  ?.emit(
@@ -5,27 +5,19 @@ package expo.modules.kotlin.viewevent
5
5
  import android.view.View
6
6
  import java.lang.ref.WeakReference
7
7
  import kotlin.reflect.KProperty
8
- import kotlin.reflect.KType
9
- import kotlin.reflect.typeOf
10
8
 
11
9
  typealias CoalescingKey<T> = (event: T) -> Short
12
10
 
13
11
  class ViewEventDelegate<T>(
14
- private val type: KType,
15
12
  view: View,
16
13
  private val coalescingKey: CoalescingKey<T>?
17
14
  ) {
18
15
  private val viewHolder = WeakReference(view)
19
- internal var isValidated = false
20
16
 
21
17
  operator fun getValue(thisRef: View, property: KProperty<*>): ViewEventCallback<T> {
22
- if (!isValidated) {
23
- throw IllegalStateException("You have to export '${property.name}' property as a event in the `View` component")
24
- }
25
-
26
18
  val view = viewHolder.get()
27
19
  ?: throw IllegalStateException("Can't send the '${property.name}' event from the view that is deallocated")
28
- return ViewEvent(property.name, type, view, coalescingKey)
20
+ return ViewEvent(property.name, view, coalescingKey)
29
21
  }
30
22
  }
31
23
 
@@ -36,11 +28,11 @@ class ViewEventDelegate<T>(
36
28
  */
37
29
  @Suppress("FunctionName")
38
30
  inline fun <reified T> View.EventDispatcher(noinline coalescingKey: CoalescingKey<T>? = null): ViewEventDelegate<T> {
39
- return ViewEventDelegate(typeOf<T>(), this, coalescingKey)
31
+ return ViewEventDelegate(this, coalescingKey)
40
32
  }
41
33
 
42
34
  @JvmName("MapEventDispatcher")
43
35
  @Suppress("FunctionName")
44
36
  fun View.EventDispatcher(coalescingKey: CoalescingKey<Map<String, Any>>? = null): ViewEventDelegate<Map<String, Any>> {
45
- return ViewEventDelegate(typeOf<Map<String, Any>>(), this, coalescingKey)
37
+ return ViewEventDelegate(this, coalescingKey)
46
38
  }
@@ -287,41 +287,37 @@ class ViewDefinitionBuilder<T : View>(
287
287
  ) = AsyncFunctionBuilder(name).also { functionBuilders[name] = it }
288
288
 
289
289
  private fun createViewFactory(): (Context, AppContext) -> View = viewFactory@{ context: Context, appContext: AppContext ->
290
- val primaryConstructor = requireNotNull(getPrimaryConstructor()) { "$viewClass doesn't have a primary constructor" }
291
- val args = primaryConstructor.parameters
292
-
293
- if (args.isEmpty()) {
294
- throw IllegalStateException("Android view has to have a constructor with at least one argument.")
295
- }
296
-
297
- val firstArgType = args.first().type
298
- if (Context::class != firstArgType.classifier) {
299
- throw IllegalStateException("The type of the first constructor argument has to be `android.content.Context`.")
290
+ val fullConstructor = try {
291
+ // Try to use constructor with two arguments
292
+ viewClass.java.getConstructor(Context::class.java, AppContext::class.java)
293
+ } catch (e: NoSuchMethodException) {
294
+ null
300
295
  }
301
296
 
302
- // Backward compatibility
303
- if (args.size == 1) {
297
+ fullConstructor?.let {
304
298
  return@viewFactory try {
305
- primaryConstructor.call(context)
299
+ it.newInstance(context, appContext)
306
300
  } catch (e: Throwable) {
307
301
  handleFailureDuringViewCreation(context, appContext, e)
308
302
  }
309
303
  }
310
304
 
311
- val secondArgType = args[1].type
312
- if (AppContext::class != secondArgType.classifier) {
313
- throw IllegalStateException("The type of the second constructor argument has to be `expo.modules.kotlin.AppContext`.")
305
+ val contextConstructor = try {
306
+ // Try to use constructor that use Android's context
307
+ viewClass.java.getConstructor(Context::class.java)
308
+ } catch (e: NoSuchMethodException) {
309
+ null
314
310
  }
315
311
 
316
- if (args.size != 2) {
317
- throw IllegalStateException("Android view has more constructor arguments than expected.")
312
+ contextConstructor?.let {
313
+ return@viewFactory try {
314
+ it.newInstance(context)
315
+ } catch (e: Throwable) {
316
+ handleFailureDuringViewCreation(context, appContext, e)
317
+ }
318
318
  }
319
319
 
320
- return@viewFactory try {
321
- primaryConstructor.call(context, appContext)
322
- } catch (e: Throwable) {
323
- handleFailureDuringViewCreation(context, appContext, e)
324
- }
320
+ throw IllegalStateException("Didn't find a correct constructor for $viewClass")
325
321
  }
326
322
 
327
323
  private fun handleFailureDuringViewCreation(context: Context, appContext: AppContext, e: Throwable): View {
@@ -4,16 +4,12 @@ import android.content.Context
4
4
  import android.view.View
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
  import com.facebook.react.common.MapBuilder
7
- import expo.modules.core.utilities.ifNull
8
7
  import expo.modules.kotlin.ModuleHolder
9
8
  import expo.modules.kotlin.events.normalizeEventName
10
9
  import expo.modules.kotlin.exception.OnViewDidUpdatePropsException
11
10
  import expo.modules.kotlin.exception.exceptionDecorator
12
11
  import expo.modules.kotlin.exception.toCodedException
13
12
  import expo.modules.kotlin.logger
14
- import expo.modules.kotlin.viewevent.ViewEventDelegate
15
- import kotlin.reflect.full.declaredMemberProperties
16
- import kotlin.reflect.jvm.isAccessible
17
13
 
18
14
  class ViewManagerWrapperDelegate(internal var moduleHolder: ModuleHolder) {
19
15
  private val definition: ViewManagerDefinition
@@ -31,9 +27,6 @@ class ViewManagerWrapperDelegate(internal var moduleHolder: ModuleHolder) {
31
27
  fun createView(context: Context): View {
32
28
  return definition
33
29
  .createView(context, moduleHolder.module.appContext)
34
- .also {
35
- configureView(it)
36
- }
37
30
  }
38
31
 
39
32
  fun onViewDidUpdateProps(view: View) {
@@ -126,33 +119,4 @@ class ViewManagerWrapperDelegate(internal var moduleHolder: ModuleHolder) {
126
119
  }
127
120
  return builder.build()
128
121
  }
129
-
130
- private fun configureView(view: View) {
131
- val callbacks = definition.callbacksDefinition?.names ?: return
132
-
133
- val kClass = view.javaClass.kotlin
134
- val propertiesMap = kClass
135
- .declaredMemberProperties
136
- .associateBy { it.name }
137
-
138
- callbacks.forEach {
139
- val property = propertiesMap[it].ifNull {
140
- logger.warn("⚠️ Property `$it` does not exist in ${kClass.simpleName}")
141
- return@forEach
142
- }
143
- property.isAccessible = true
144
-
145
- val delegate = property.getDelegate(view).ifNull {
146
- logger.warn("⚠️ Property delegate for `$it` in ${kClass.simpleName} does not exist")
147
- return@forEach
148
- }
149
-
150
- val viewDelegate = (delegate as? ViewEventDelegate<*>).ifNull {
151
- logger.warn("⚠️ Property delegate for `$it` cannot be cased to `ViewCallbackDelegate`")
152
- return@forEach
153
- }
154
-
155
- viewDelegate.isValidated = true
156
- }
157
- }
158
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -42,5 +42,5 @@
42
42
  "@testing-library/react-hooks": "^7.0.1",
43
43
  "expo-module-scripts": "^3.0.0"
44
44
  },
45
- "gitHead": "7a1079dcba56e0bb7504210049b6195b64f13834"
45
+ "gitHead": "25af32e83a26dc8027223b61bf7459ba93310ba3"
46
46
  }