Skip to content

Commit c57607b

Browse files
committed
temp: simplify implementation
1 parent 3a5520e commit c57607b

File tree

6 files changed

+227
-167
lines changed

6 files changed

+227
-167
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.wix.detox.inquiry
2+
3+
import android.util.Log
4+
import com.facebook.react.bridge.ReactContext
5+
import com.facebook.react.fabric.FabricUIManager
6+
import com.facebook.react.uimanager.UIManagerHelper
7+
// import com.facebook.react.uimanager.UIManagerType
8+
9+
/**
10+
* Main entry point for Detox animation tracking in Fabric.
11+
* This provides a simple API to initialize and use the animation tracking system.
12+
*/
13+
object DetoxAnimationTracker {
14+
private const val LOG_TAG = "DetoxAnimationTracker"
15+
private var isInitialized = false
16+
17+
/**
18+
* Initialize the animation tracking system.
19+
* This should be called once when Detox starts up.
20+
*/
21+
fun initialize(reactContext: ReactContext) {
22+
if (isInitialized) {
23+
Log.d(LOG_TAG, "DetoxAnimationTracker already initialized")
24+
return
25+
}
26+
27+
try {
28+
// Initialize the Fabric integration
29+
DetoxFabricIntegration.initialize(reactContext)
30+
isInitialized = true
31+
Log.i(LOG_TAG, "DetoxAnimationTracker initialized successfully")
32+
33+
} catch (e: Exception) {
34+
Log.e(LOG_TAG, "Failed to initialize DetoxAnimationTracker", e)
35+
}
36+
}
37+
38+
/**
39+
* Get the current animation statistics
40+
*/
41+
fun getAnimationStats(): Map<String, Any> {
42+
return ViewLifecycleRegistry.getStats()
43+
}
44+
45+
/**
46+
* Get all recently animated views
47+
*/
48+
fun getRecentlyAnimatedViews(): List<android.view.View> {
49+
return ViewLifecycleRegistry.getRecentlyAnimatedViews()
50+
}
51+
52+
/**
53+
* Check if a specific view was recently animated
54+
*/
55+
fun wasRecentlyAnimated(view: android.view.View): Boolean {
56+
return ViewLifecycleRegistry.wasRecentlyAnimated(view)
57+
}
58+
59+
/**
60+
* Clear all animation tracking data
61+
*/
62+
fun clear() {
63+
ViewLifecycleRegistry.clear()
64+
}
65+
66+
/**
67+
* Check if the tracker is initialized
68+
*/
69+
fun isInitialized(): Boolean = isInitialized
70+
}

detox/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricAnimationHook.kt

Lines changed: 16 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,37 @@ package com.wix.detox.inquiry
33
import android.util.Log
44
import android.view.View
55
import com.facebook.react.bridge.ReadableMap
6-
import com.facebook.react.fabric.mounting.SurfaceMountingManager
6+
import com.facebook.react.fabric.FabricUIManager
77
import com.wix.detox.inquiry.ViewLifecycleRegistry.markAnimated
88
import com.wix.detox.inquiry.ViewLifecycleRegistry.markMounted
99
import com.wix.detox.inquiry.ViewLifecycleRegistry.markUpdated
10-
import com.wix.detox.inquiry.ViewLifecycleRegistry.markCustomEvent
1110

1211
/**
13-
* Hook into React Native's Fabric new architecture to track view lifecycle events.
14-
* This hooks into the exact points where views are mounted, updated, and animated.
12+
* Hook into React Native's Fabric new architecture to track animated views.
13+
* This provides precise tracking by intercepting the exact points where animated
14+
* properties are applied to views in Fabric.
1515
*/
1616
object DetoxFabricAnimationHook {
1717
private const val LOG_TAG = "DetoxFabricHook"
1818

1919
/**
20-
* Hook into IntBufferBatchMountItem.execute() to track animated view updates.
21-
* This is called when animated props are applied to views in Fabric.
20+
* Hook into FabricUIManager.synchronouslyUpdateViewOnUIThread to track animated updates.
21+
* This marks views as animated whenever there's any animation activity, giving lots of false positives.
2222
*/
23-
fun hookIntBufferBatchMountItem(
24-
viewTag: Int,
23+
fun hookSynchronouslyUpdateViewOnUIThread(
24+
reactTag: Int,
2525
props: ReadableMap?,
26-
surfaceMountingManager: SurfaceMountingManager
26+
fabricUIManager: FabricUIManager
2727
) {
2828
try {
2929
// Get the actual Android View
30-
val androidView = getViewByTag(surfaceMountingManager, viewTag)
30+
val androidView = fabricUIManager.resolveView(reactTag)
3131
if (androidView == null) {
32-
Log.d(LOG_TAG, "View not found for tag: $viewTag")
32+
Log.d(LOG_TAG, "View not found for tag: $reactTag")
3333
return
3434
}
3535

36-
// Check if this is an animated update
37-
if (isAnimatedPropsUpdate(props)) {
38-
Log.d(LOG_TAG, "Animated props update for view tag: $viewTag")
39-
markAnimated(androidView)
40-
41-
// Log problematic animations
42-
if (isProblematicAnimation(props)) {
43-
Log.w(LOG_TAG, "Problematic animation detected for view tag: $viewTag")
44-
markCustomEvent(androidView, "problematic_animation")
45-
}
46-
} else {
47-
// Regular props update
48-
markUpdated(androidView)
49-
}
50-
36+
markAnimated(androidView)
5137
} catch (e: Exception) {
5238
Log.w(LOG_TAG, "Failed to hook animated view update", e)
5339
}
@@ -57,100 +43,20 @@ object DetoxFabricAnimationHook {
5743
* Hook into view mount operations to track when views are created.
5844
*/
5945
fun hookViewMount(
60-
viewTag: Int,
61-
surfaceMountingManager: SurfaceMountingManager
46+
reactTag: Int,
47+
fabricUIManager: FabricUIManager
6248
) {
6349
try {
64-
val androidView = getViewByTag(surfaceMountingManager, viewTag)
50+
val androidView = fabricUIManager.resolveView(reactTag)
6551
if (androidView != null) {
66-
Log.d(LOG_TAG, "View mounted with tag: $viewTag")
52+
Log.d(LOG_TAG, "View mounted with tag: $reactTag")
6753
markMounted(androidView)
6854
}
6955
} catch (e: Exception) {
7056
Log.w(LOG_TAG, "Failed to hook view mount", e)
7157
}
7258
}
7359

74-
/**
75-
* Get Android View by React Native view tag using reflection.
76-
* This works around the fact that SurfaceMountingManager doesn't expose a direct getView method.
77-
*/
78-
private fun getViewByTag(
79-
surfaceMountingManager: SurfaceMountingManager,
80-
viewTag: Int
81-
): View? {
82-
return try {
83-
// Use reflection to access the internal view registry
84-
val viewRegistryField = surfaceMountingManager.javaClass.getDeclaredField("mViewRegistry")
85-
viewRegistryField.isAccessible = true
86-
val viewRegistry = viewRegistryField.get(surfaceMountingManager)
87-
88-
// Get the view from the registry
89-
val getViewMethod = viewRegistry.javaClass.getMethod("getView", Int::class.java)
90-
getViewMethod.invoke(viewRegistry, viewTag) as? View
91-
} catch (e: Exception) {
92-
Log.w(LOG_TAG, "Failed to get view by tag: $viewTag", e)
93-
null
94-
}
95-
}
96-
97-
/**
98-
* Check if this is an animated props update by looking for animated properties.
99-
*/
100-
private fun isAnimatedPropsUpdate(props: ReadableMap?): Boolean {
101-
if (props == null) return false
102-
103-
val animatedKeys = setOf(
104-
"transform", "opacity", "scaleX", "scaleY", "scale",
105-
"translateX", "translateY", "rotateX", "rotateY", "rotateZ",
106-
"backgroundColor", "borderRadius", "borderWidth"
107-
)
108-
109-
val iterator = props.keySetIterator()
110-
while (iterator.hasNextKey()) {
111-
val key = iterator.nextKey()
112-
if (animatedKeys.any { key.contains(it, ignoreCase = true) }) {
113-
return true
114-
}
115-
}
116-
117-
return false
118-
}
119-
120-
/**
121-
* Check if this animation might be problematic (infinite loops, conflicting animations, etc.).
122-
*/
123-
private fun isProblematicAnimation(props: ReadableMap?): Boolean {
124-
if (props == null) return false
125-
126-
// Check for potential infinite loop patterns
127-
val transformKeys = mutableSetOf<String>()
128-
val iterator = props.keySetIterator()
129-
130-
while (iterator.hasNextKey()) {
131-
val key = iterator.nextKey()
132-
if (key.contains("transform", ignoreCase = true)) {
133-
transformKeys.add(key)
134-
}
135-
}
136-
137-
// Multiple transform properties might indicate conflicting animations
138-
if (transformKeys.size > 3) {
139-
Log.w(LOG_TAG, "Multiple transform properties detected: $transformKeys")
140-
return true
141-
}
142-
143-
// Check for opacity animations that might cause issues
144-
if (props.hasKey("opacity")) {
145-
val opacity = props.getDouble("opacity")
146-
if (opacity < 0.0 || opacity > 1.0) {
147-
Log.w(LOG_TAG, "Invalid opacity value: $opacity")
148-
return true
149-
}
150-
}
151-
152-
return false
153-
}
15460

15561
/**
15662
* Get view coordinates for highlighting
@@ -167,21 +73,6 @@ object DetoxFabricAnimationHook {
16773
return coords
16874
}
16975

170-
/**
171-
* Get view coordinates relative to root view
172-
*/
173-
fun getViewCoordinatesRelativeToRoot(view: View, rootView: View): IntArray {
174-
val viewCoords = getViewCoordinates(view)
175-
val rootCoords = getViewCoordinates(rootView)
176-
177-
return intArrayOf(
178-
viewCoords[0] - rootCoords[0], // Relative X
179-
viewCoords[1] - rootCoords[1], // Relative Y
180-
viewCoords[2], // Width
181-
viewCoords[3] // Height
182-
)
183-
}
184-
18576
/**
18677
* Log current registry statistics
18778
*/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.wix.detox.inquiry
2+
3+
import android.util.Log
4+
import com.facebook.react.bridge.ReactContext
5+
import com.facebook.react.fabric.FabricUIManager
6+
import com.facebook.react.uimanager.UIManagerHelper
7+
// import com.facebook.react.uimanager.UIManagerType
8+
9+
/**
10+
* Integration point for Detox with React Native's Fabric architecture.
11+
* This provides hooks into Fabric's animation system to track animated views.
12+
*/
13+
object DetoxFabricIntegration {
14+
private const val LOG_TAG = "DetoxFabricIntegration"
15+
private var isInitialized = false
16+
17+
/**
18+
* Initialize the Fabric animation hooks.
19+
* This should be called once when Detox starts up.
20+
*/
21+
fun initialize(reactContext: ReactContext) {
22+
if (isInitialized) {
23+
Log.d(LOG_TAG, "DetoxFabricIntegration already initialized")
24+
return
25+
}
26+
27+
try {
28+
// Get the FabricUIManager
29+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, 1) as? FabricUIManager
30+
if (fabricUIManager == null) {
31+
Log.w(LOG_TAG, "FabricUIManager not available - Fabric animation tracking disabled")
32+
return
33+
}
34+
35+
// Hook into the FabricUIManager
36+
hookFabricUIManager(fabricUIManager)
37+
isInitialized = true
38+
Log.i(LOG_TAG, "DetoxFabricIntegration initialized successfully")
39+
40+
} catch (e: Exception) {
41+
Log.e(LOG_TAG, "Failed to initialize DetoxFabricIntegration", e)
42+
}
43+
}
44+
45+
/**
46+
* Hook into FabricUIManager to track animated updates.
47+
* This uses reflection to intercept synchronouslyUpdateViewOnUIThread calls.
48+
*/
49+
private fun hookFabricUIManager(fabricUIManager: FabricUIManager) {
50+
try {
51+
// Create a wrapper that intercepts calls to synchronouslyUpdateViewOnUIThread
52+
val originalMethod = FabricUIManager::class.java.getDeclaredMethod(
53+
"synchronouslyUpdateViewOnUIThread",
54+
Int::class.java,
55+
com.facebook.react.bridge.ReadableMap::class.java
56+
)
57+
58+
// Note: In a real implementation, you would use bytecode manipulation
59+
// or AOP to intercept this method. For now, we'll provide a manual hook
60+
// that can be called from the application code.
61+
62+
Log.d(LOG_TAG, "FabricUIManager hook prepared (manual integration required)")
63+
64+
} catch (e: Exception) {
65+
Log.w(LOG_TAG, "Failed to hook FabricUIManager", e)
66+
}
67+
}
68+
69+
/**
70+
* Manual hook for synchronouslyUpdateViewOnUIThread.
71+
* This should be called from the application's FabricUIManager wrapper.
72+
*/
73+
fun onSynchronouslyUpdateViewOnUIThread(
74+
reactTag: Int,
75+
props: com.facebook.react.bridge.ReadableMap?,
76+
fabricUIManager: FabricUIManager
77+
) {
78+
DetoxFabricAnimationHook.hookSynchronouslyUpdateViewOnUIThread(reactTag, props, fabricUIManager)
79+
}
80+
81+
/**
82+
* Manual hook for view mount operations.
83+
*/
84+
fun onViewMount(reactTag: Int, fabricUIManager: FabricUIManager) {
85+
DetoxFabricAnimationHook.hookViewMount(reactTag, fabricUIManager)
86+
}
87+
88+
/**
89+
* Check if the integration is initialized
90+
*/
91+
fun isInitialized(): Boolean = isInitialized
92+
93+
/**
94+
* Get current animation statistics
95+
*/
96+
fun getAnimationStats(): Map<String, Any> {
97+
return ViewLifecycleRegistry.getStats()
98+
}
99+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.wix.detox.inquiry
2+
3+
import android.util.Log
4+
import com.facebook.react.bridge.ReadableMap
5+
import com.facebook.react.fabric.FabricUIManager
6+
7+
/**
8+
* Wrapper for FabricUIManager that intercepts animation-related calls.
9+
* This provides a clean way to hook into Fabric's animation system.
10+
*/
11+
class DetoxFabricUIManagerWrapper(
12+
private val originalUIManager: FabricUIManager
13+
) {
14+
private val LOG_TAG = "DetoxFabricUIManagerWrapper"
15+
16+
fun synchronouslyUpdateViewOnUIThread(reactTag: Int, props: ReadableMap?) {
17+
try {
18+
// Call the original method first (only if props is not null)
19+
if (props != null) {
20+
originalUIManager.synchronouslyUpdateViewOnUIThread(reactTag, props)
21+
}
22+
23+
// Then hook our animation tracking
24+
DetoxFabricAnimationHook.hookSynchronouslyUpdateViewOnUIThread(reactTag, props, originalUIManager)
25+
26+
} catch (e: Exception) {
27+
Log.w(LOG_TAG, "Failed to process animated view update", e)
28+
// Still call the original method to avoid breaking the app (only if props is not null)
29+
if (props != null) {
30+
originalUIManager.synchronouslyUpdateViewOnUIThread(reactTag, props)
31+
}
32+
}
33+
}
34+
35+
// Delegate all other methods to the original UIManager
36+
fun resolveView(reactTag: Int) = originalUIManager.resolveView(reactTag)
37+
}

0 commit comments

Comments
 (0)