Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android(feat):new arch support #4675

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions detox/android/detox/proguard-rules-app.pro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
-keep class com.facebook.react.animated.** { *; }
-keep class com.facebook.react.ReactApplication { *; }
-keep class com.facebook.react.ReactNativeHost { *; }
-keep class com.facebook.react.ReactHost { *; }
-keep class com.facebook.react.runtime.ReactHostImpl { *; }

-keep class com.facebook.react.ReactInstanceManager { *; }
-keep class com.facebook.react.ReactInstanceManager** { *; }
-keep class com.facebook.react.ReactInstanceEventListener { *; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ object DetoxMain {
* not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good
* thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow.
*/
@Synchronized
private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
awaitHandshake()
launchActivity(rnHostHolder, activityLaunchHelper)
synchronized(this) {
awaitHandshake()
launchActivity(rnHostHolder, activityLaunchHelper)
}
}

private fun awaitHandshake() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.wix.detox.reactnative

import android.annotation.SuppressLint
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.bridge.ReactContext
import org.joor.Reflect


fun ReactApplication.getInstanceManagerSafe(): ReactInstanceManager {
return reactNativeHost.reactInstanceManager
?: throw RuntimeException("ReactInstanceManager is null!")
}

@SuppressLint("VisibleForTests")
fun ReactApplication.getCurrentReactContextSafe(): ReactContext? {
return if (isFabricEnabled()) {
reactHost?.currentReactContext
} else {
getInstanceManagerSafe().currentReactContext
}
}

/**
* A method to check if Fabric is enabled in the React Native application.
*/
fun ReactApplication.isFabricEnabled(): Boolean {
return Reflect.on(reactNativeHost).call("getUIManagerProvider") != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import android.content.Context
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.bridge.ReactContext
import com.wix.detox.LaunchArgs
import com.wix.detox.reactnative.idlingresources.ReactNativeIdlingResources
import com.wix.detox.reactnative.reloader.ReactNativeReLoader
import com.wix.detox.reactnative.reloader.ReactNativeReloaderFactory

private const val LOG_TAG = "DetoxRNExt"

Expand All @@ -34,9 +35,9 @@ object ReactNativeExtension {
}

(applicationContext as ReactApplication).let {
val reactContext = awaitNewReactNativeContext(it, null)
awaitNewReactNativeContext(it, null)

enableOrDisableSynchronization(reactContext)
enableOrDisableSynchronization(it)
}
}

Expand All @@ -59,12 +60,12 @@ object ReactNativeExtension {
(applicationContext as ReactApplication).let {
clearIdlingResources()

val previousReactContext = getCurrentReactContextSafe(it)
val previousReactContext = it.getCurrentReactContextSafe()

reloadReactNativeInBackground(it)
val reactContext = awaitNewReactNativeContext(it, previousReactContext)
awaitNewReactNativeContext(it, previousReactContext)

enableOrDisableSynchronization(reactContext)
enableOrDisableSynchronization(it)
}
}

Expand All @@ -75,11 +76,7 @@ object ReactNativeExtension {

@JvmStatic
fun enableAllSynchronization(applicationContext: ReactApplication) {
val reactContext = getCurrentReactContextSafe(applicationContext)

if (reactContext != null) {
setupIdlingResources(reactContext)
}
setupIdlingResources(applicationContext)
}

@JvmStatic
Expand All @@ -88,7 +85,7 @@ object ReactNativeExtension {
@JvmStatic
fun getRNActivity(applicationContext: Context): Activity? {
if (ReactNativeInfo.isReactNativeApp()) {
return getCurrentReactContextSafe(applicationContext as ReactApplication)?.currentActivity
return (applicationContext as ReactApplication).getCurrentReactContextSafe()?.currentActivity
}
return null
}
Expand All @@ -115,20 +112,27 @@ object ReactNativeExtension {
}

private fun reloadReactNativeInBackground(reactApplication: ReactApplication) {
val rnReloader = ReactNativeReLoader(InstrumentationRegistry.getInstrumentation(), reactApplication)
val rnReloader = ReactNativeReloaderFactory(InstrumentationRegistry.getInstrumentation(), reactApplication).create()
rnReloader.reloadInBackground()
}

private fun awaitNewReactNativeContext(reactApplication: ReactApplication, previousReactContext: ReactContext?): ReactContext {
val rnLoadingMonitor = ReactNativeLoadingMonitor(InstrumentationRegistry.getInstrumentation(), reactApplication, previousReactContext)
private fun awaitNewReactNativeContext(
reactApplication: ReactApplication,
previousReactContext: ReactContext?
): ReactContext {
val rnLoadingMonitor = ReactNativeLoadingMonitor(
InstrumentationRegistry.getInstrumentation(),
reactApplication,
previousReactContext
)
return rnLoadingMonitor.getNewContext()!!
}

private fun enableOrDisableSynchronization(reactContext: ReactContext) {
private fun enableOrDisableSynchronization(reactApplication: ReactApplication) {
if (shouldDisableSynchronization()) {
clearAllSynchronization()
} else {
setupIdlingResources(reactContext)
setupIdlingResources(reactApplication)
}
}

Expand All @@ -137,10 +141,10 @@ object ReactNativeExtension {
return launchArgs.hasEnableSynchronization() && launchArgs.enableSynchronization.equals("0")
}

private fun setupIdlingResources(reactContext: ReactContext) {
private fun setupIdlingResources(reactApplication: ReactApplication) {
val launchArgs = LaunchArgs()

rnIdlingResources = ReactNativeIdlingResources(reactContext, launchArgs).apply {
rnIdlingResources = ReactNativeIdlingResources(reactApplication, launchArgs).apply {
registerAll()
}
}
Expand All @@ -150,12 +154,4 @@ object ReactNativeExtension {
rnIdlingResources = null
}

private fun getInstanceManagerSafe(reactApplication: ReactApplication): ReactInstanceManager {
return reactApplication.reactNativeHost.reactInstanceManager
?: throw RuntimeException("ReactInstanceManager is null!")
}

private fun getCurrentReactContextSafe(reactApplication: ReactApplication): ReactContext? {
return getInstanceManagerSafe(reactApplication).currentReactContext
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@ package com.wix.detox.reactnative
import android.app.Instrumentation
import android.util.Log
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.runtime.ReactHostImpl
import com.wix.detox.common.DetoxErrors
import com.wix.detox.config.DetoxConfig
import org.joor.Reflect
import java.lang.reflect.Proxy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

private const val LOG_TAG = "DetoxRNLoading"

private const val REACT_INSTANCE_EVENT_LISTENER_CLASS = "com.facebook.react.ReactInstanceEventListener"
private const val REACT_INSTANCE_EVENT_LISTENER_CLASS_COMPAT = "com.facebook.react.ReactInstanceManager\$ReactInstanceEventListener"

open class ReactNativeLoadingMonitor(
private val instrumentation: Instrumentation,
private val rnApplication: ReactApplication,
private val previousReactContext: ReactContext?,
private val config: DetoxConfig = DetoxConfig.CONFIG) {
private val instrumentation: Instrumentation,
private val rnApplication: ReactApplication,
private val previousReactContext: ReactContext?,
private val config: DetoxConfig = DetoxConfig.CONFIG
) {
private val countDownLatch = CountDownLatch(1)

fun getNewContext(): ReactContext? {
Expand All @@ -31,24 +28,21 @@ open class ReactNativeLoadingMonitor(

private fun subscribeToNewRNContextUpdates() {
instrumentation.runOnMainSync(
Runnable {
val rnInstanceManager = rnApplication.reactNativeHost.reactInstanceManager
val reactContext = rnInstanceManager.currentReactContext
if (reactContext != null && reactContext !== previousReactContext) {
Log.d(LOG_TAG, "Got new RN-context directly and immediately")
countDownLatch.countDown()
return@Runnable
}
Runnable {
val reactContext = rnApplication.getCurrentReactContextSafe()
if (reactContext != null && reactContext !== previousReactContext) {
Log.d(LOG_TAG, "Got new RN-context directly and immediately")
countDownLatch.countDown()
return@Runnable
}

subscribeAsyncRNContextHandler(rnInstanceManager) {
countDownLatch.countDown()
}
})
subscribeAsyncRNContextHandler() {
countDownLatch.countDown()
}
})
}

private fun awaitNewRNContext(): ReactContext? {
val rnInstanceManager = rnApplication.reactNativeHost.reactInstanceManager

var i = 0
while (true) {
try {
Expand All @@ -58,9 +52,10 @@ open class ReactNativeLoadingMonitor(
// First load can take a lot of time. (packager)
// Loads afterwards should take less than a second.
throw DetoxErrors.DetoxRuntimeException(
"""Waited for the new RN-context for too long! (${config.rnContextLoadTimeoutSec} seconds)
"""Waited for the new RN-context for too long! (${config.rnContextLoadTimeoutSec} seconds)
|If you think that's not long enough, consider applying a custom Detox runtime-config in DetoxTest.runTests()."""
.trimMargin())
.trimMargin()
)
}
} else {
break
Expand All @@ -69,8 +64,10 @@ open class ReactNativeLoadingMonitor(
// Due to an ugly timing issue in RN
// it is possible that our listener won't be ever called
// That's why we have to check the reactContext regularly.
val reactContext = rnInstanceManager.currentReactContext
if (reactContext != null && reactContext !== previousReactContext) {
val reactContext = rnApplication.getCurrentReactContextSafe()

// We also need to wait for rect native instance to be initialized
if (reactContext != null && reactContext !== previousReactContext && reactContext.hasActiveReactInstance()) {
Log.d(LOG_TAG, "Got new RN-context explicitly while polling (#iteration=$i)")
break
}
Expand All @@ -79,51 +76,31 @@ open class ReactNativeLoadingMonitor(
}
}

return rnInstanceManager.currentReactContext
return rnApplication.getCurrentReactContextSafe()
}
}

private interface DummyListenerIdentifier

/**
* This baby bridges over RN's breaking change introduced in version 0.68:
* `ReactInstanceManager$ReactInstanceEventListener` was extracted onto a separate interface, having
* `ReactInstanceManager.{add|remove}ReactInstanceEventLister()` changing their signature to use it, accordingly.
*
* This made us resort to a solution based on dynamic proxies, because - depending on RN's version (at runtime), we
* need to dynamically decide what interface we "extend" (or actually shadow) via the proxy.
*
* @see RNDiff https://github.com/facebook/react-native/compare/v0.67.4..v0.68.0#diff-2f01f0cd7ff8c9ea58f12ef0eff5fa8250cad144dd5490598d80d8e9e743458aR1009
* @see DynamicProxies https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
*/
private fun subscribeAsyncRNContextHandler(rnInstanceManager: ReactInstanceManager, onReactContextInitialized: () -> Any) {
val listenerClass = resolveListenerClass()
val proxyInterfaces = arrayOf(
listenerClass,
DummyListenerIdentifier::class.java // In order to be able to implement equals()
)
val listener = Proxy.newProxyInstance(listenerClass.classLoader, proxyInterfaces) { listener, method, args ->
Log.d(LOG_TAG, "Listener-proxy method called: ${method.name}")

val result = when (method.name) {
"onReactContextInitialized" -> {
Log.i(LOG_TAG, "Got new RN-context async'ly through listener")
Reflect.on(rnInstanceManager).call("removeReactInstanceEventListener", listener)
onReactContextInitialized()
}
"equals" -> {
val candidate = args[0]
candidate is DummyListenerIdentifier
}
else -> Any()
private fun subscribeAsyncRNContextHandler(onReactContextInitialized: () -> Any) {
val isFabric = rnApplication.isFabricEnabled()
if (isFabric) {
// We do a casting for supporting RN 0.75 and above
val host = rnApplication.reactHost as ReactHostImpl?
host?.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
Log.i(LOG_TAG, "Got new RN-context through listener")
onReactContextInitialized()
host.removeReactInstanceEventListener(this)
}
})
} else {
val rnInstanceManager = rnApplication.getInstanceManagerSafe()
rnInstanceManager.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
Log.i(LOG_TAG, "Got new RN-context directly through listener")
onReactContextInitialized()
rnInstanceManager.removeReactInstanceEventListener(this)
}
})
}

result
}
Reflect.on(rnInstanceManager).call("addReactInstanceEventListener", listener)
}

private fun resolveListenerClass(): Class<*> {
val className = if (ReactNativeInfo.rnVersion().minor >= 68) REACT_INSTANCE_EVENT_LISTENER_CLASS else REACT_INSTANCE_EVENT_LISTENER_CLASS_COMPAT
return Class.forName(className)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,7 @@ object RNHelpers {
fun getNativeModule(reactContext: ReactContext, className: String): NativeModule? =
try {
val moduleClass = Class.forName(className) as Class<NativeModule>

if (reactContext.hasNativeModule(moduleClass)) {
reactContext.getNativeModule(moduleClass)
} else {
Log.d(LOG_TAG, "Native RN module resolution (class $className): no registered module found")
null
}
reactContext.getNativeModule(moduleClass)
} catch (ex: ClassNotFoundException) {
Log.d(LOG_TAG, "Native RN module resolution (class $className): no such class")
null
Expand Down
Loading
Loading