@callstack/react-native-brownfield 5.3.0 → 6.0.1
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 +44 -0
- package/android/build.gradle +1 -0
- package/android/consumer-rules.pro +6 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformance.kt +384 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformanceModule.kt +15 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt +8 -5
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +78 -23
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldPackage.kt +1 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +89 -3
- package/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt +2 -0
- package/android/src/test/java/com/callstack/reactnativebrownfield/PerformanceMetricsTest.kt +54 -0
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +59 -15
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +6 -8
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +6 -4
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/RNBundleTaskStubs.kt +27 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
- package/ios/BrownfieldDisplayMetrics.swift +265 -0
- package/ios/BrownfieldPerformanceBridge.h +8 -0
- package/ios/BrownfieldPerformanceBridge.mm +103 -0
- package/ios/Expo/ExpoHostRuntime.swift +60 -14
- package/ios/Expo/ExpoUpdatesStartup.swift +50 -0
- package/ios/JSBundleLoadObserver.swift +56 -19
- package/ios/JSBundleTimingObserver.h +26 -0
- package/ios/JSBundleTimingObserver.m +140 -0
- package/ios/ReactHostPreloading.swift +7 -3
- package/ios/ReactNativeBrownfield.swift +82 -14
- package/ios/ReactNativeBrownfield.xcodeproj/project.pbxproj +16 -0
- package/ios/Vanilla/ReactNativeHostRuntime.swift +1 -1
- package/ios/Views/ReactNativeView.swift +21 -3
- package/ios/Views/ReactNativeViewController.swift +44 -42
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/commonjs/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/lib/commonjs/index.js +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scaffold/index.js +1 -1
- package/lib/commonjs/scaffold/index.js.map +1 -1
- package/lib/commonjs/scaffold/projectFiles.js +1 -1
- package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/module/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/scaffold/index.js +1 -1
- package/lib/module/scaffold/index.js.map +1 -1
- package/lib/module/scaffold/projectFiles.js +1 -1
- package/lib/module/scaffold/projectFiles.js.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +7 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +7 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
- package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
- package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/src/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/src/index.ts +13 -1
- package/src/scaffold/index.ts +60 -4
- package/src/scaffold/projectFiles.ts +5 -3
|
@@ -2,6 +2,7 @@ package com.callstack.reactnativebrownfield
|
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import android.os.Bundle
|
|
5
|
+
import android.view.WindowManager
|
|
5
6
|
import android.widget.FrameLayout
|
|
6
7
|
import androidx.activity.OnBackPressedCallback
|
|
7
8
|
import androidx.fragment.app.FragmentActivity
|
|
@@ -20,7 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList
|
|
|
20
21
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
21
22
|
|
|
22
23
|
fun interface OnJSBundleLoaded {
|
|
23
|
-
operator fun invoke(
|
|
24
|
+
operator fun invoke(timings: JSBundleTimings)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
fun interface OnMessageListener {
|
|
@@ -34,6 +35,7 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
34
35
|
private lateinit var instance: ReactNativeBrownfield
|
|
35
36
|
private val initialized = AtomicBoolean()
|
|
36
37
|
private val nativeLibsLoaded = AtomicBoolean()
|
|
38
|
+
private lateinit var applicationContext: Application
|
|
37
39
|
|
|
38
40
|
@JvmStatic
|
|
39
41
|
val shared: ReactNativeBrownfield get() = instance
|
|
@@ -62,7 +64,9 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
62
64
|
reactHost: ReactHost,
|
|
63
65
|
onJSBundleLoaded: OnJSBundleLoaded? = null
|
|
64
66
|
) {
|
|
67
|
+
applicationContext = application
|
|
65
68
|
if (!initialized.getAndSet(true)) {
|
|
69
|
+
AndroidJSBundleTimingObserver.install()
|
|
66
70
|
loadNativeLibs(application)
|
|
67
71
|
installAndPreload(reactHost, onJSBundleLoaded)
|
|
68
72
|
} else {
|
|
@@ -76,14 +80,16 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
76
80
|
onJSBundleLoaded: OnJSBundleLoaded? = null,
|
|
77
81
|
reactHostFactory: () -> ReactHost
|
|
78
82
|
) {
|
|
83
|
+
applicationContext = application
|
|
79
84
|
if (!initialized.getAndSet(true)) {
|
|
85
|
+
AndroidJSBundleTimingObserver.install()
|
|
80
86
|
loadNativeLibs(application)
|
|
81
87
|
installAndPreload(reactHostFactory(), onJSBundleLoaded)
|
|
82
88
|
} else {
|
|
83
89
|
invokeWhenJSBundleLoaded(onJSBundleLoaded)
|
|
84
90
|
}
|
|
85
91
|
}
|
|
86
|
-
|
|
92
|
+
|
|
87
93
|
@JvmStatic
|
|
88
94
|
@JvmOverloads
|
|
89
95
|
fun initialize(
|
|
@@ -91,7 +97,9 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
91
97
|
options: HashMap<String, Any>,
|
|
92
98
|
onJSBundleLoaded: OnJSBundleLoaded? = null
|
|
93
99
|
) {
|
|
100
|
+
applicationContext = application
|
|
94
101
|
if (!initialized.getAndSet(true)) {
|
|
102
|
+
AndroidJSBundleTimingObserver.install()
|
|
95
103
|
loadNativeLibs(application)
|
|
96
104
|
val reactHost = getDefaultReactHost(
|
|
97
105
|
context = application,
|
|
@@ -123,11 +131,11 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
123
131
|
initialize(application, options, onJSBundleLoaded)
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
private fun preloadReactNative(
|
|
134
|
+
private fun preloadReactNative(generation: Int) {
|
|
127
135
|
shared.reactHost.addReactInstanceEventListener(object :
|
|
128
136
|
ReactInstanceEventListener {
|
|
129
137
|
override fun onReactContextInitialized(context: ReactContext) {
|
|
130
|
-
|
|
138
|
+
AndroidJSBundleTimingObserver.contextInitialized(context, generation)
|
|
131
139
|
shared.reactHost.removeReactInstanceEventListener(this)
|
|
132
140
|
}
|
|
133
141
|
})
|
|
@@ -135,10 +143,12 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
private fun installAndPreload(reactHost: ReactHost, onJSBundleLoaded: OnJSBundleLoaded?) {
|
|
146
|
+
AndroidJSBundleTimingObserver.install()
|
|
147
|
+
val generation = AndroidJSBundleTimingObserver.beginGeneration()
|
|
138
148
|
instance = ReactNativeBrownfield(reactHost)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
reactHost.installBrownfieldPerformanceStop()
|
|
150
|
+
AndroidJSBundleTimingObserver.subscribe(onJSBundleLoaded)
|
|
151
|
+
preloadReactNative(generation)
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
private fun invokeWhenJSBundleLoaded(onJSBundleLoaded: OnJSBundleLoaded?) {
|
|
@@ -146,18 +156,21 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
146
156
|
return
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
onJSBundleLoaded.invoke(true)
|
|
152
|
-
return
|
|
153
|
-
}
|
|
159
|
+
AndroidJSBundleTimingObserver.subscribe(onJSBundleLoaded)
|
|
160
|
+
}
|
|
154
161
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
internal fun currentReactContext(): ReactContext? =
|
|
163
|
+
if (::instance.isInitialized) instance.reactHost.currentReactContext else null
|
|
164
|
+
|
|
165
|
+
internal fun displayFrameIntervalNanos(): Long {
|
|
166
|
+
if (!::applicationContext.isInitialized) return 1_000_000_000L / 60
|
|
167
|
+
@Suppress("DEPRECATION")
|
|
168
|
+
val refreshRate = applicationContext
|
|
169
|
+
.getSystemService(WindowManager::class.java)
|
|
170
|
+
?.defaultDisplay
|
|
171
|
+
?.refreshRate
|
|
172
|
+
?.takeIf { it > 0f } ?: 60f
|
|
173
|
+
return (1_000_000_000.0 / refreshRate).toLong()
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
|
|
@@ -215,10 +228,47 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
215
228
|
reactDelegate: ReactDelegateWrapper?,
|
|
216
229
|
launchOptions: Bundle?,
|
|
217
230
|
lifecycleOwner: LifecycleOwner?,
|
|
231
|
+
): FrameLayout {
|
|
232
|
+
return createViewInternal(
|
|
233
|
+
activity, moduleName, reactDelegate, launchOptions, lifecycleOwner, null, false
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@JvmOverloads
|
|
238
|
+
fun createView(
|
|
239
|
+
activity: FragmentActivity?,
|
|
240
|
+
moduleName: String,
|
|
241
|
+
reactDelegate: ReactDelegateWrapper? = null,
|
|
242
|
+
launchOptions: Bundle? = null,
|
|
243
|
+
waitForFullDisplay: Boolean,
|
|
244
|
+
collectThreadMetrics: Boolean = false,
|
|
245
|
+
onMetrics: OnDisplayMetrics? = null,
|
|
246
|
+
): FrameLayout {
|
|
247
|
+
val session = BrownfieldDisplaySession(
|
|
248
|
+
moduleName, waitForFullDisplay, collectThreadMetrics, onMetrics
|
|
249
|
+
)
|
|
250
|
+
val props = Bundle(launchOptions ?: Bundle()).apply {
|
|
251
|
+
putString("brownfieldPresentationID", session.id)
|
|
252
|
+
}
|
|
253
|
+
return createViewInternal(activity, moduleName, reactDelegate, props, activity, session, false)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
internal fun createViewInternal(
|
|
257
|
+
activity: FragmentActivity?,
|
|
258
|
+
moduleName: String,
|
|
259
|
+
reactDelegate: ReactDelegateWrapper?,
|
|
260
|
+
launchOptions: Bundle?,
|
|
261
|
+
lifecycleOwner: LifecycleOwner?,
|
|
262
|
+
session: BrownfieldDisplaySession?,
|
|
263
|
+
requiresResume: Boolean,
|
|
218
264
|
): FrameLayout {
|
|
219
265
|
val reactHost = shared.reactHost
|
|
220
|
-
val resolvedDelegate =
|
|
221
|
-
reactDelegate
|
|
266
|
+
val resolvedDelegate = when {
|
|
267
|
+
reactDelegate == null -> ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
|
|
268
|
+
session != null && reactDelegate.brownfieldLaunchOptions !== launchOptions ->
|
|
269
|
+
reactDelegate.withLaunchOptions(launchOptions)
|
|
270
|
+
else -> reactDelegate
|
|
271
|
+
}
|
|
222
272
|
|
|
223
273
|
val backPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
|
|
224
274
|
override fun handleOnBackPressed() {
|
|
@@ -248,24 +298,29 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
|
|
|
248
298
|
* In such a case, we set the lifeCycle observer.
|
|
249
299
|
*/
|
|
250
300
|
if (reactDelegate == null) {
|
|
251
|
-
lifecycleOwner?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
|
|
301
|
+
lifecycleOwner?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate, session))
|
|
252
302
|
}
|
|
253
303
|
|
|
254
304
|
resolvedDelegate.loadApp()
|
|
255
|
-
return resolvedDelegate.reactRootView
|
|
305
|
+
return resolvedDelegate.reactRootView!!.also { session?.bind(it, requiresResume) }
|
|
256
306
|
}
|
|
257
307
|
|
|
258
|
-
private fun getLifeCycleObserver(
|
|
308
|
+
private fun getLifeCycleObserver(
|
|
309
|
+
reactDelegate: ReactDelegateWrapper,
|
|
310
|
+
session: BrownfieldDisplaySession?
|
|
311
|
+
): DefaultLifecycleObserver {
|
|
259
312
|
return object : DefaultLifecycleObserver {
|
|
260
313
|
override fun onResume(owner: LifecycleOwner) {
|
|
261
314
|
reactDelegate.onReactHostResume()
|
|
262
315
|
}
|
|
263
316
|
|
|
264
317
|
override fun onPause(owner: LifecycleOwner) {
|
|
318
|
+
session?.cancel()
|
|
265
319
|
reactDelegate.onHostPause()
|
|
266
320
|
}
|
|
267
321
|
|
|
268
322
|
override fun onDestroy(owner: LifecycleOwner) {
|
|
323
|
+
session?.cancel()
|
|
269
324
|
reactDelegate.onHostDestroy()
|
|
270
325
|
owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
|
|
271
326
|
}
|
package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldPackage.kt
CHANGED
|
@@ -17,6 +17,7 @@ class ReactNativeBrownfieldPackage : ReactPackage {
|
|
|
17
17
|
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
|
|
18
18
|
val modules = ArrayList<NativeModule>()
|
|
19
19
|
modules.add(ReactNativeBrownfieldModule(reactContext))
|
|
20
|
+
modules.add(BrownfieldPerformanceModule(reactContext))
|
|
20
21
|
return modules
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -17,6 +17,10 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
17
17
|
private lateinit var permissionsCallback: Callback
|
|
18
18
|
private var permissionListener: PermissionListener? = null
|
|
19
19
|
private lateinit var moduleName: String
|
|
20
|
+
private var displaySession: BrownfieldDisplaySession? = null
|
|
21
|
+
|
|
22
|
+
/** Reattach after Fragment restoration; callbacks are intentionally not serialized. */
|
|
23
|
+
var onMetrics: OnDisplayMetrics? = null
|
|
20
24
|
|
|
21
25
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
22
26
|
/**
|
|
@@ -51,12 +55,34 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
51
55
|
container: ViewGroup?,
|
|
52
56
|
savedInstanceState: Bundle?
|
|
53
57
|
): View {
|
|
54
|
-
|
|
58
|
+
val args = arguments
|
|
59
|
+
// Only the measured factories set this argument. Plain fragments keep their props as is.
|
|
60
|
+
val isMeasured = args?.containsKey(ReactNativeFragmentArgNames.ARG_WAIT_FOR_FULL_DISPLAY) == true
|
|
61
|
+
val session = if (isMeasured) {
|
|
62
|
+
BrownfieldDisplaySession(
|
|
63
|
+
moduleName,
|
|
64
|
+
args!!.getBoolean(ReactNativeFragmentArgNames.ARG_WAIT_FOR_FULL_DISPLAY, false),
|
|
65
|
+
args.getBoolean(ReactNativeFragmentArgNames.ARG_COLLECT_THREAD_METRICS, false),
|
|
66
|
+
onMetrics
|
|
67
|
+
)
|
|
68
|
+
} else {
|
|
69
|
+
null
|
|
70
|
+
}
|
|
71
|
+
displaySession = session
|
|
72
|
+
val launchOptions = Bundle(
|
|
73
|
+
args?.getBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS) ?: Bundle()
|
|
74
|
+
).apply { session?.let { putString("brownfieldPresentationID", it.id) } }
|
|
75
|
+
this.reactDelegate = ReactDelegateWrapper(
|
|
76
|
+
activity, this.reactHost, moduleName, launchOptions
|
|
77
|
+
)
|
|
78
|
+
return ReactNativeBrownfield.shared.createViewInternal(
|
|
55
79
|
activity,
|
|
56
80
|
moduleName,
|
|
57
81
|
this.reactDelegate as ReactDelegateWrapper,
|
|
58
|
-
launchOptions
|
|
59
|
-
|
|
82
|
+
launchOptions,
|
|
83
|
+
viewLifecycleOwner,
|
|
84
|
+
session,
|
|
85
|
+
true
|
|
60
86
|
)
|
|
61
87
|
}
|
|
62
88
|
|
|
@@ -69,9 +95,11 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
69
95
|
} catch (_: ClassCastException) {
|
|
70
96
|
(this.reactDelegate as ReactDelegateWrapper).onReactHostResume()
|
|
71
97
|
}
|
|
98
|
+
displaySession?.setResumed(true)
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
override fun onPause() {
|
|
102
|
+
displaySession?.cancel()
|
|
75
103
|
try {
|
|
76
104
|
super.onPause()
|
|
77
105
|
} catch (error: AssertionError) {
|
|
@@ -91,6 +119,12 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
91
119
|
}
|
|
92
120
|
}
|
|
93
121
|
|
|
122
|
+
override fun onDestroyView() {
|
|
123
|
+
displaySession?.cancel()
|
|
124
|
+
displaySession = null
|
|
125
|
+
super.onDestroyView()
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
override fun onRequestPermissionsResult(
|
|
95
129
|
requestCode: Int,
|
|
96
130
|
permissions: Array<String>,
|
|
@@ -143,6 +177,26 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
143
177
|
return fragment
|
|
144
178
|
}
|
|
145
179
|
|
|
180
|
+
@JvmStatic
|
|
181
|
+
@JvmOverloads
|
|
182
|
+
fun createReactNativeFragment(
|
|
183
|
+
moduleName: String,
|
|
184
|
+
initialProps: Bundle?,
|
|
185
|
+
waitForFullDisplay: Boolean,
|
|
186
|
+
collectThreadMetrics: Boolean = false,
|
|
187
|
+
onMetrics: OnDisplayMetrics? = null
|
|
188
|
+
): ReactNativeFragment {
|
|
189
|
+
return createReactNativeFragment(moduleName, initialProps).also { fragment ->
|
|
190
|
+
fragment.arguments?.putBoolean(
|
|
191
|
+
ReactNativeFragmentArgNames.ARG_WAIT_FOR_FULL_DISPLAY, waitForFullDisplay
|
|
192
|
+
)
|
|
193
|
+
fragment.arguments?.putBoolean(
|
|
194
|
+
ReactNativeFragmentArgNames.ARG_COLLECT_THREAD_METRICS, collectThreadMetrics
|
|
195
|
+
)
|
|
196
|
+
fragment.onMetrics = onMetrics
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
146
200
|
@JvmStatic
|
|
147
201
|
fun createReactNativeFragment(
|
|
148
202
|
moduleName: String,
|
|
@@ -151,6 +205,22 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
151
205
|
return createReactNativeFragment(moduleName, PropsBundle.fromHashMap(initialProps))
|
|
152
206
|
}
|
|
153
207
|
|
|
208
|
+
@JvmStatic
|
|
209
|
+
@JvmOverloads
|
|
210
|
+
fun createReactNativeFragment(
|
|
211
|
+
moduleName: String,
|
|
212
|
+
initialProps: HashMap<String, *>,
|
|
213
|
+
waitForFullDisplay: Boolean,
|
|
214
|
+
collectThreadMetrics: Boolean = false,
|
|
215
|
+
onMetrics: OnDisplayMetrics? = null
|
|
216
|
+
): ReactNativeFragment = createReactNativeFragment(
|
|
217
|
+
moduleName,
|
|
218
|
+
PropsBundle.fromHashMap(initialProps),
|
|
219
|
+
waitForFullDisplay,
|
|
220
|
+
collectThreadMetrics,
|
|
221
|
+
onMetrics
|
|
222
|
+
)
|
|
223
|
+
|
|
154
224
|
@JvmStatic
|
|
155
225
|
fun createReactNativeFragment(
|
|
156
226
|
moduleName: String,
|
|
@@ -158,5 +228,21 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
158
228
|
): ReactNativeFragment {
|
|
159
229
|
return createReactNativeFragment(moduleName, initialProps.toHashMap())
|
|
160
230
|
}
|
|
231
|
+
|
|
232
|
+
@JvmStatic
|
|
233
|
+
@JvmOverloads
|
|
234
|
+
fun createReactNativeFragment(
|
|
235
|
+
moduleName: String,
|
|
236
|
+
initialProps: WritableMap,
|
|
237
|
+
waitForFullDisplay: Boolean,
|
|
238
|
+
collectThreadMetrics: Boolean = false,
|
|
239
|
+
onMetrics: OnDisplayMetrics? = null
|
|
240
|
+
): ReactNativeFragment = createReactNativeFragment(
|
|
241
|
+
moduleName,
|
|
242
|
+
initialProps.toHashMap(),
|
|
243
|
+
waitForFullDisplay,
|
|
244
|
+
collectThreadMetrics,
|
|
245
|
+
onMetrics
|
|
246
|
+
)
|
|
161
247
|
}
|
|
162
248
|
}
|
|
@@ -16,5 +16,7 @@ class ReactNativeFragmentArgNames private constructor() :
|
|
|
16
16
|
|
|
17
17
|
// re-export constants from ReactFragment to enable access
|
|
18
18
|
const val ARG_LAUNCH_OPTIONS: String = "arg_launch_options"
|
|
19
|
+
const val ARG_WAIT_FOR_FULL_DISPLAY = "arg_wait_for_full_display"
|
|
20
|
+
const val ARG_COLLECT_THREAD_METRICS = "arg_collect_thread_metrics"
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.callstack.reactnativebrownfield
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class PerformanceMetricsTest {
|
|
8
|
+
@Test fun `pairs markers and sorts overlapping timeline`() {
|
|
9
|
+
val samples = JSBundleTimingAccumulator()
|
|
10
|
+
samples.record(BundleMarker.EVALUATION_START, 1, 20)
|
|
11
|
+
samples.record(BundleMarker.DOWNLOAD_START, 0, 10)
|
|
12
|
+
samples.record(BundleMarker.EVALUATION_END, 1, 40)
|
|
13
|
+
samples.record(BundleMarker.DOWNLOAD_END, 0, 30)
|
|
14
|
+
|
|
15
|
+
val result = samples.snapshot()
|
|
16
|
+
assertEquals(20.0, result.jsBundleLoadTime!!, 0.0)
|
|
17
|
+
assertEquals(20.0, result.jsBundleEvaluationTime!!, 0.0)
|
|
18
|
+
assertEquals(listOf("DOWNLOAD", "RUN_JS_BUNDLE"), result.timeline.map { it["tag"] })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test fun `keeps zero duration and rejects missing ambiguous and other instances`() {
|
|
22
|
+
val zero = JSBundleTimingAccumulator().apply {
|
|
23
|
+
record(BundleMarker.EVALUATION_START, 1, 5)
|
|
24
|
+
record(BundleMarker.EVALUATION_END, 1, 5)
|
|
25
|
+
}.snapshot()
|
|
26
|
+
assertEquals(0.0, zero.jsBundleEvaluationTime!!, 0.0)
|
|
27
|
+
|
|
28
|
+
val missing = JSBundleTimingAccumulator().apply {
|
|
29
|
+
record(BundleMarker.DOWNLOAD_END, 0, 8)
|
|
30
|
+
record(BundleMarker.EVALUATION_START, 2, 9)
|
|
31
|
+
record(BundleMarker.EVALUATION_END, 2, 10)
|
|
32
|
+
}.snapshot()
|
|
33
|
+
assertNull(missing.jsBundleLoadTime)
|
|
34
|
+
assertNull(missing.jsBundleEvaluationTime)
|
|
35
|
+
|
|
36
|
+
val ambiguous = JSBundleTimingAccumulator().apply {
|
|
37
|
+
record(BundleMarker.DOWNLOAD_START, 0, 1)
|
|
38
|
+
record(BundleMarker.DOWNLOAD_START, 0, 2)
|
|
39
|
+
record(BundleMarker.DOWNLOAD_END, 0, 3)
|
|
40
|
+
}.snapshot()
|
|
41
|
+
assertNull(ambiguous.jsBundleLoadTime)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test fun `frame accumulator includes unfinished deadline excess`() {
|
|
45
|
+
val samples = BrownfieldFrameSamples()
|
|
46
|
+
samples.begin(0)
|
|
47
|
+
samples.record(20_000_000, 10_000_000)
|
|
48
|
+
val result = samples.snapshot(50_000_000)
|
|
49
|
+
assertEquals(20.0, result.fps!!, 0.001)
|
|
50
|
+
assertEquals(0.6, result.busyRatio!!, 0.001)
|
|
51
|
+
assertEquals(50.0, result.sampledMs, 0.001)
|
|
52
|
+
assertEquals(1, result.frameCount)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -3,13 +3,16 @@ package com.callstack.react.brownfield.plugin
|
|
|
3
3
|
import com.android.build.api.dsl.AndroidSourceSet
|
|
4
4
|
import com.android.build.api.dsl.LibraryExtension
|
|
5
5
|
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
|
6
|
+
import com.android.build.api.variant.SourceDirectories
|
|
6
7
|
import com.callstack.react.brownfield.exceptions.NameSpaceNotFound
|
|
7
8
|
import com.callstack.react.brownfield.processors.VariantHelper
|
|
8
9
|
import com.callstack.react.brownfield.utils.Extension
|
|
9
10
|
import com.callstack.react.brownfield.utils.Utils
|
|
10
11
|
import com.callstack.react.brownfield.utils.capitalized
|
|
12
|
+
import org.gradle.api.GradleException
|
|
11
13
|
import org.gradle.api.Project
|
|
12
14
|
import org.gradle.api.Task
|
|
15
|
+
import org.gradle.api.file.DirectoryProperty
|
|
13
16
|
import org.gradle.api.tasks.Copy
|
|
14
17
|
import java.io.File
|
|
15
18
|
|
|
@@ -63,26 +66,67 @@ object RNSourceSets {
|
|
|
63
66
|
val capitalizedBundledAssetsVariantName = bundledAssetsVariantName.capitalized()
|
|
64
67
|
val appProject = getAppProject()
|
|
65
68
|
|
|
66
|
-
val
|
|
67
|
-
listOf(
|
|
68
|
-
// outputs for RN <= 0.81
|
|
69
|
-
"createBundle${capitalizedBundledAssetsVariantName}JsAndAssets",
|
|
70
|
-
// outputs for RN >= 0.82
|
|
71
|
-
"react/$bundledAssetsVariantName",
|
|
72
|
-
)
|
|
69
|
+
val jsBundleTaskName = "createBundle${capitalizedBundledAssetsVariantName}JsAndAssets"
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Wire the bundle task's own `jsBundleDir`/`resourcesDir` outputs as *generated*
|
|
73
|
+
* source directories
|
|
74
|
+
*/
|
|
75
|
+
wireGeneratedSourceDirectory(appProject, jsBundleTaskName, "getJsBundleDir", variant.sources.assets)
|
|
76
|
+
wireGeneratedSourceDirectory(appProject, jsBundleTaskName, "getResourcesDir", variant.sources.res)
|
|
79
77
|
|
|
80
78
|
if (Utils.hasExpoUpdates(appProject, variantName)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/**
|
|
80
|
+
* expo-updates registers `create<Variant>UpdatesResources` as
|
|
81
|
+
* `ExpoUpdatesPlugin.CreateUpdatesResourcesTask`, with a single
|
|
82
|
+
* `assetDir: DirectoryProperty` output (no `res` output exists for this task).
|
|
83
|
+
*/
|
|
84
|
+
val updatesResourcesTaskName = VariantHelper.getExpoUpdatesResourcesTaskName(variant.name)
|
|
85
|
+
wireGeneratedSourceDirectory(appProject, updatesResourcesTaskName, "getAssetDir", variant.sources.assets)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wires `taskName`'s `getterName` output (expected to be a `DirectoryProperty`) as a
|
|
92
|
+
* generated source directory for `sources`.
|
|
93
|
+
*
|
|
94
|
+
* Resolving the task by name (rather than importing its class) keeps this plugin from
|
|
95
|
+
* depending on RNGP's/expo-updates' internal task types. If the task, or its expected output
|
|
96
|
+
* property, isn't there, this fails the build immediately with a clear message instead of
|
|
97
|
+
* silently producing an empty/stale source directory.
|
|
98
|
+
*/
|
|
99
|
+
private fun wireGeneratedSourceDirectory(
|
|
100
|
+
appProject: Project,
|
|
101
|
+
taskName: String,
|
|
102
|
+
getterName: String,
|
|
103
|
+
sources: SourceDirectories?,
|
|
104
|
+
) {
|
|
105
|
+
if (sources == null) return
|
|
106
|
+
|
|
107
|
+
val task =
|
|
108
|
+
appProject.tasks.findByName(taskName)
|
|
109
|
+
?: throw GradleException(
|
|
110
|
+
"brownfield: expected task '$taskName' was not found in project " +
|
|
111
|
+
"'${appProject.path}'. Check that the plugin producing it is applied there.",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
val getter =
|
|
115
|
+
runCatching { task.javaClass.getMethod(getterName) }.getOrElse {
|
|
116
|
+
throw GradleException(
|
|
117
|
+
"brownfield: task '$taskName' (${task.javaClass.name}) does not expose " +
|
|
118
|
+
"'$getterName()'. The plugin that registers it most likely changed its " +
|
|
119
|
+
"API; the brownfield Gradle plugin needs updating to match.",
|
|
120
|
+
it,
|
|
121
|
+
)
|
|
84
122
|
}
|
|
123
|
+
|
|
124
|
+
check(DirectoryProperty::class.java.isAssignableFrom(getter.returnType)) {
|
|
125
|
+
"brownfield: '$taskName.$getterName()' no longer returns a DirectoryProperty."
|
|
85
126
|
}
|
|
127
|
+
|
|
128
|
+
val taskProvider = appProject.tasks.named(taskName)
|
|
129
|
+
sources.addGeneratedSourceDirectory(taskProvider) { t -> getter.invoke(t) as DirectoryProperty }
|
|
86
130
|
}
|
|
87
131
|
|
|
88
132
|
private fun getLibraryNameSpace(): String {
|
|
@@ -3,6 +3,7 @@ package com.callstack.react.brownfield.utils
|
|
|
3
3
|
import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
|
|
4
4
|
import org.gradle.api.Project
|
|
5
5
|
import java.io.File
|
|
6
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
6
7
|
|
|
7
8
|
class AndroidArchiveLibrary(
|
|
8
9
|
private val project: Project,
|
|
@@ -46,11 +47,47 @@ class AndroidArchiveLibrary(
|
|
|
46
47
|
val regex = Regex("""namespace\s*=?\s*["']([^"']+)["']""")
|
|
47
48
|
val match = regex.find(text)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return match?.groupValues?.get(1)
|
|
51
|
+
?: getNameSpaceFromAndroidManifest(subProj)
|
|
52
|
+
?: error(
|
|
53
|
+
"No namespace found in ${buildFile.path}, and no package attribute found in " +
|
|
54
|
+
"AndroidManifest.xml for ${artifact.moduleName}",
|
|
55
|
+
)
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Falls back to the `package` attribute of the dependency's `AndroidManifest.xml` when the
|
|
60
|
+
* module declares no `namespace` in its `build.gradle`. The Android Gradle plugin itself
|
|
61
|
+
* derives the namespace this way for such libraries, so they build fine as a regular
|
|
62
|
+
* dependency but trip up [getNameSpaceFromBuildGradle]'s `build.gradle`-only regex.
|
|
63
|
+
*
|
|
64
|
+
* The manifest is parsed as XML and only the root `<manifest>` element's `package`
|
|
65
|
+
* attribute is read, so comments or child attributes such as `android:targetPackage`
|
|
66
|
+
* cannot be mistaken for the package name.
|
|
67
|
+
*/
|
|
68
|
+
private fun getNameSpaceFromAndroidManifest(subProj: Project): String? {
|
|
69
|
+
val candidateManifests =
|
|
70
|
+
listOf(
|
|
71
|
+
File(subProj.projectDir, "src/main/AndroidManifest.xml"),
|
|
72
|
+
File(subProj.projectDir, "AndroidManifest.xml"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return candidateManifests
|
|
76
|
+
.filter { it.exists() }
|
|
77
|
+
.firstNotNullOfOrNull { manifestFile ->
|
|
78
|
+
runCatching {
|
|
79
|
+
val document =
|
|
80
|
+
DocumentBuilderFactory.newInstance().apply {
|
|
81
|
+
isNamespaceAware = true
|
|
82
|
+
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
|
|
83
|
+
setFeature("http://xml.org/sax/features/external-general-entities", false)
|
|
84
|
+
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
|
|
85
|
+
}.newDocumentBuilder().parse(manifestFile)
|
|
86
|
+
document.documentElement
|
|
87
|
+
?.getAttribute("package")
|
|
88
|
+
?.takeIf { it.isNotBlank() }
|
|
89
|
+
}.getOrNull()
|
|
90
|
+
}
|
|
54
91
|
}
|
|
55
92
|
|
|
56
93
|
fun getManifestFile() = File(getExplodedAarRootDir(), "AndroidManifest.xml")
|
|
@@ -102,9 +102,10 @@ class AgpCompatibilityMatrixTest {
|
|
|
102
102
|
File(projectDir, "local.properties").writeText("sdk.dir=$androidSdk\n")
|
|
103
103
|
// Stands in for the RN app project. It has to be a real `com.android.application` because
|
|
104
104
|
// the brownfield plugin wires into AGP's own app-side tasks; the extra registrations cover
|
|
105
|
-
// the tasks the React Native Gradle plugin would otherwise contribute.
|
|
106
|
-
//
|
|
107
|
-
|
|
105
|
+
// the tasks the React Native Gradle plugin would otherwise contribute. The bundle tasks need
|
|
106
|
+
// their `jsBundleDir`/`resourcesDir` outputs, which the plugin wires as generated sources;
|
|
107
|
+
// the rest can stay empty, since this test is about AGP API compatibility.
|
|
108
|
+
writeRNBundleTaskStubs(File(projectDir, "app").apply { mkdirs() })
|
|
108
109
|
File(projectDir, "app/build.gradle.kts").writeText(
|
|
109
110
|
"""
|
|
110
111
|
plugins {
|
|
@@ -116,11 +117,8 @@ class AgpCompatibilityMatrixTest {
|
|
|
116
117
|
compileSdk = 34
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"createBundleDebugJsAndAssets",
|
|
122
|
-
"createBundleReleaseJsAndAssets",
|
|
123
|
-
).forEach { name -> tasks.register(name) }
|
|
120
|
+
tasks.register("generateAutolinkingPackageList")
|
|
121
|
+
apply(from = "$RN_BUNDLE_TASK_STUBS_SCRIPT")
|
|
124
122
|
""".trimIndent(),
|
|
125
123
|
)
|
|
126
124
|
|
|
@@ -64,10 +64,12 @@ class LegacyConsumerConfigurationTest {
|
|
|
64
64
|
""".trimIndent(),
|
|
65
65
|
)
|
|
66
66
|
File(projectDir, "local.properties").writeText("sdk.dir=$androidSdk\n")
|
|
67
|
-
// ArtifactsResolver looks for an `app` project
|
|
68
|
-
// the task container,
|
|
69
|
-
|
|
70
|
-
File(projectDir, "app
|
|
67
|
+
// ArtifactsResolver looks for an `app` project. It only needs the RN bundle tasks whose
|
|
68
|
+
// outputs the plugin wires as generated sources; this test is about the task container,
|
|
69
|
+
// not discovery.
|
|
70
|
+
val appDir = File(projectDir, "app").apply { mkdirs() }
|
|
71
|
+
writeRNBundleTaskStubs(appDir)
|
|
72
|
+
File(appDir, "build.gradle.kts").writeText("apply(from = \"$RN_BUNDLE_TASK_STUBS_SCRIPT\")\n")
|
|
71
73
|
|
|
72
74
|
// The `tasks.register("removeDependenciesFromModuleFile")` block below is verbatim from
|
|
73
75
|
// the published setup docs. Do not "modernize" it — its whole purpose is to reproduce
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.plugin
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
|
|
5
|
+
internal const val RN_BUNDLE_TASK_STUBS_SCRIPT = "rn-bundle-task-stubs.gradle.kts"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Writes a script plugin into [appDir] that stubs RNGP's `createBundle<Variant>JsAndAssets` tasks.
|
|
9
|
+
* The brownfield plugin wires their `jsBundleDir`/`resourcesDir` outputs as generated sources and
|
|
10
|
+
* fails fast when they are missing, so plain `tasks.register(name)` stubs are not enough.
|
|
11
|
+
* Apply it from the app build script with `apply(from = RN_BUNDLE_TASK_STUBS_SCRIPT)`.
|
|
12
|
+
*/
|
|
13
|
+
internal fun writeRNBundleTaskStubs(appDir: File) {
|
|
14
|
+
File(appDir, RN_BUNDLE_TASK_STUBS_SCRIPT).writeText(
|
|
15
|
+
"""
|
|
16
|
+
abstract class StubBundleTask : DefaultTask() {
|
|
17
|
+
@get:OutputDirectory abstract val jsBundleDir: DirectoryProperty
|
|
18
|
+
@get:OutputDirectory abstract val resourcesDir: DirectoryProperty
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
listOf(
|
|
22
|
+
"createBundleDebugJsAndAssets",
|
|
23
|
+
"createBundleReleaseJsAndAssets",
|
|
24
|
+
).forEach { name -> tasks.register<StubBundleTask>(name) }
|
|
25
|
+
""".trimIndent(),
|
|
26
|
+
)
|
|
27
|
+
}
|