@oneuptime/react-native-replay 13.0.4
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/OneUptimeReactNativeReplay.podspec +20 -0
- package/README.md +264 -0
- package/android/build.gradle +36 -0
- package/android/consumer-rules.pro +26 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/oneuptime/replay/OneUptimeReplayPackage.java +29 -0
- package/android/src/main/java/com/oneuptime/replay/OneUptimeReplayViewTreeModule.java +541 -0
- package/dist/index.cjs +4111 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +4088 -0
- package/dist/index.js.map +7 -0
- package/dist/types/ChunkBuffer.d.ts +47 -0
- package/dist/types/ChunkBuffer.d.ts.map +1 -0
- package/dist/types/Config.d.ts +59 -0
- package/dist/types/Config.d.ts.map +1 -0
- package/dist/types/Contract.d.ts +176 -0
- package/dist/types/Contract.d.ts.map +1 -0
- package/dist/types/Events.d.ts +14 -0
- package/dist/types/Events.d.ts.map +1 -0
- package/dist/types/MobileReplayRecorder.d.ts +176 -0
- package/dist/types/MobileReplayRecorder.d.ts.map +1 -0
- package/dist/types/NativeViewTree.d.ts +49 -0
- package/dist/types/NativeViewTree.d.ts.map +1 -0
- package/dist/types/Outbox.d.ts +29 -0
- package/dist/types/Outbox.d.ts.map +1 -0
- package/dist/types/ReplayProvider.d.ts +28 -0
- package/dist/types/ReplayProvider.d.ts.map +1 -0
- package/dist/types/Sanitize.d.ts +29 -0
- package/dist/types/Sanitize.d.ts.map +1 -0
- package/dist/types/Storage.d.ts +30 -0
- package/dist/types/Storage.d.ts.map +1 -0
- package/dist/types/TouchPrivacy.d.ts +15 -0
- package/dist/types/TouchPrivacy.d.ts.map +1 -0
- package/dist/types/Transport.d.ts +36 -0
- package/dist/types/Transport.d.ts.map +1 -0
- package/dist/types/ViewTreeSerializer.d.ts +29 -0
- package/dist/types/ViewTreeSerializer.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/ios/OneUptimeReplayViewTreeModule.m +17 -0
- package/ios/OneUptimeReplayViewTreeModule.swift +239 -0
- package/package.json +66 -0
- package/react-native.config.js +15 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
package com.oneuptime.replay;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color;
|
|
4
|
+
import android.graphics.drawable.ColorDrawable;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.view.SurfaceView;
|
|
7
|
+
import android.view.TextureView;
|
|
8
|
+
import android.view.View;
|
|
9
|
+
import android.view.ViewGroup;
|
|
10
|
+
import android.webkit.WebView;
|
|
11
|
+
import android.widget.Button;
|
|
12
|
+
import android.widget.EditText;
|
|
13
|
+
import android.widget.ImageView;
|
|
14
|
+
import android.widget.ScrollView;
|
|
15
|
+
import android.widget.TextView;
|
|
16
|
+
|
|
17
|
+
import androidx.annotation.NonNull;
|
|
18
|
+
|
|
19
|
+
import com.facebook.react.bridge.Arguments;
|
|
20
|
+
import com.facebook.react.bridge.Promise;
|
|
21
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
22
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
23
|
+
import com.facebook.react.bridge.UIManager;
|
|
24
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
25
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
26
|
+
import com.facebook.react.bridge.WritableArray;
|
|
27
|
+
import com.facebook.react.bridge.WritableMap;
|
|
28
|
+
import com.facebook.react.uimanager.RootView;
|
|
29
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
30
|
+
|
|
31
|
+
import java.lang.reflect.Method;
|
|
32
|
+
import java.util.Locale;
|
|
33
|
+
|
|
34
|
+
public final class OneUptimeReplayViewTreeModule extends ReactContextBaseJavaModule {
|
|
35
|
+
private static final String REPLAY_MASK_TEST_ID = "oneuptime-replay-mask";
|
|
36
|
+
private static final int MAX_TREE_DEPTH = 64;
|
|
37
|
+
private static final int MAX_TREE_NODES = 5000;
|
|
38
|
+
|
|
39
|
+
private static final class TraversalState {
|
|
40
|
+
int visited = 0;
|
|
41
|
+
int truncated = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
OneUptimeReplayViewTreeModule(ReactApplicationContext reactContext) {
|
|
45
|
+
super(reactContext);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@NonNull
|
|
49
|
+
@Override
|
|
50
|
+
public String getName() {
|
|
51
|
+
return "OneUptimeReplayViewTree";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
public void captureViewTree(double rootTagValue, Promise promise) {
|
|
56
|
+
final int rootTag = (int) rootTagValue;
|
|
57
|
+
UiThreadUtil.runOnUiThread(() -> {
|
|
58
|
+
try {
|
|
59
|
+
UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(
|
|
60
|
+
getReactApplicationContext(),
|
|
61
|
+
rootTag
|
|
62
|
+
);
|
|
63
|
+
if (uiManager == null) {
|
|
64
|
+
promise.reject("E_UI_MANAGER", "React Native UIManager is unavailable.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
View root = uiManager.resolveView(rootTag);
|
|
69
|
+
if (root == null) {
|
|
70
|
+
promise.reject("E_ROOT_VIEW", "Replay root view was not found.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
int[] rootLocation = new int[2];
|
|
75
|
+
root.getLocationOnScreen(rootLocation);
|
|
76
|
+
TraversalState state = new TraversalState();
|
|
77
|
+
WritableMap serialized = serializeView(
|
|
78
|
+
root,
|
|
79
|
+
rootLocation[0],
|
|
80
|
+
rootLocation[1],
|
|
81
|
+
0,
|
|
82
|
+
true,
|
|
83
|
+
state
|
|
84
|
+
);
|
|
85
|
+
if (serialized == null) {
|
|
86
|
+
promise.reject("E_EMPTY_ROOT", "Replay root view could not be serialized.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
serialized.putInt("truncatedNodes", state.truncated);
|
|
90
|
+
promise.resolve(serialized);
|
|
91
|
+
} catch (Throwable exception) {
|
|
92
|
+
promise.reject("E_CAPTURE", "Unable to serialize the native view tree.", exception);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@ReactMethod
|
|
98
|
+
public void getAppMetadata(Promise promise) {
|
|
99
|
+
try {
|
|
100
|
+
android.content.pm.PackageInfo packageInfo = getReactApplicationContext()
|
|
101
|
+
.getPackageManager()
|
|
102
|
+
.getPackageInfo(getReactApplicationContext().getPackageName(), 0);
|
|
103
|
+
android.content.pm.ApplicationInfo applicationInfo =
|
|
104
|
+
getReactApplicationContext().getApplicationInfo();
|
|
105
|
+
WritableMap metadata = Arguments.createMap();
|
|
106
|
+
metadata.putString(
|
|
107
|
+
"appName",
|
|
108
|
+
getReactApplicationContext().getPackageManager()
|
|
109
|
+
.getApplicationLabel(applicationInfo).toString()
|
|
110
|
+
);
|
|
111
|
+
metadata.putString(
|
|
112
|
+
"appVersion",
|
|
113
|
+
packageInfo.versionName == null ? "unknown" : packageInfo.versionName
|
|
114
|
+
);
|
|
115
|
+
metadata.putString("osName", "android");
|
|
116
|
+
metadata.putString("osVersion", Build.VERSION.RELEASE);
|
|
117
|
+
promise.resolve(metadata);
|
|
118
|
+
} catch (Throwable exception) {
|
|
119
|
+
promise.reject("E_METADATA", "Unable to read application metadata.", exception);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@ReactMethod
|
|
124
|
+
public void isTouchTargetPrivate(
|
|
125
|
+
double targetTagValue,
|
|
126
|
+
double replayRootTagValue,
|
|
127
|
+
Promise promise
|
|
128
|
+
) {
|
|
129
|
+
if (!isReactTag(targetTagValue) || !isReactTag(replayRootTagValue)) {
|
|
130
|
+
promise.resolve(true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
final int targetTag = (int) targetTagValue;
|
|
135
|
+
final int replayRootTag = (int) replayRootTagValue;
|
|
136
|
+
UiThreadUtil.runOnUiThread(() -> {
|
|
137
|
+
try {
|
|
138
|
+
View target = resolveView(targetTag);
|
|
139
|
+
View replayRoot = resolveView(replayRootTag);
|
|
140
|
+
if (target == null || replayRoot == null) {
|
|
141
|
+
promise.resolve(true);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
View current = target;
|
|
146
|
+
while (current != null) {
|
|
147
|
+
if (isReplayMask(current) || isOpaqueView(current)) {
|
|
148
|
+
promise.resolve(true);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (current == replayRoot) {
|
|
152
|
+
promise.resolve(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
android.view.ViewParent parent = current.getParent();
|
|
157
|
+
current = parent instanceof View ? (View) parent : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* The event target was not a descendant of this replay root. */
|
|
161
|
+
promise.resolve(true);
|
|
162
|
+
} catch (Throwable ignored) {
|
|
163
|
+
/* Privacy decisions fail closed on stale tags or renderer errors. */
|
|
164
|
+
promise.resolve(true);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private WritableMap serializeView(
|
|
170
|
+
View view,
|
|
171
|
+
int rootX,
|
|
172
|
+
int rootY,
|
|
173
|
+
int depth,
|
|
174
|
+
boolean isRoot,
|
|
175
|
+
TraversalState state
|
|
176
|
+
) {
|
|
177
|
+
if (depth > MAX_TREE_DEPTH || state.visited >= MAX_TREE_NODES) {
|
|
178
|
+
state.truncated += 1;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!isRoot && (view.getVisibility() != View.VISIBLE
|
|
182
|
+
|| view.getWidth() <= 0 || view.getHeight() <= 0)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
state.visited += 1;
|
|
186
|
+
int[] location = new int[2];
|
|
187
|
+
view.getLocationOnScreen(location);
|
|
188
|
+
boolean masked = isReplayMask(view);
|
|
189
|
+
String kind = classify(view, masked);
|
|
190
|
+
boolean opaque = masked || "image".equals(kind) || "webview".equals(kind)
|
|
191
|
+
|| "canvas".equals(kind);
|
|
192
|
+
|
|
193
|
+
WritableMap node = Arguments.createMap();
|
|
194
|
+
node.putInt(
|
|
195
|
+
"nativeId",
|
|
196
|
+
view.getId() == View.NO_ID ? System.identityHashCode(view) : view.getId()
|
|
197
|
+
);
|
|
198
|
+
node.putString("kind", kind);
|
|
199
|
+
node.putDouble("x", toDp(view, location[0] - rootX));
|
|
200
|
+
node.putDouble("y", toDp(view, location[1] - rootY));
|
|
201
|
+
node.putDouble("width", toDp(view, view.getWidth()));
|
|
202
|
+
node.putDouble("height", toDp(view, view.getHeight()));
|
|
203
|
+
node.putBoolean("masked", masked);
|
|
204
|
+
node.putBoolean("opaque", opaque);
|
|
205
|
+
if (isRoot) {
|
|
206
|
+
View reactTouchRoot = findReactTouchRoot(view);
|
|
207
|
+
if (reactTouchRoot != null) {
|
|
208
|
+
int[] touchRootLocation = new int[2];
|
|
209
|
+
reactTouchRoot.getLocationOnScreen(touchRootLocation);
|
|
210
|
+
node.putDouble(
|
|
211
|
+
"touchOriginX",
|
|
212
|
+
toDp(view, location[0] - touchRootLocation[0])
|
|
213
|
+
);
|
|
214
|
+
node.putDouble(
|
|
215
|
+
"touchOriginY",
|
|
216
|
+
toDp(view, location[1] - touchRootLocation[1])
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
node.putDouble("opacity", view.getAlpha());
|
|
221
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
222
|
+
node.putDouble("zIndex", view.getZ());
|
|
223
|
+
}
|
|
224
|
+
putVisualStyles(view, node);
|
|
225
|
+
|
|
226
|
+
WritableArray children = Arguments.createArray();
|
|
227
|
+
if (!opaque && view instanceof ViewGroup) {
|
|
228
|
+
ViewGroup group = (ViewGroup) view;
|
|
229
|
+
for (int index = 0; index < group.getChildCount(); index += 1) {
|
|
230
|
+
WritableMap child = serializeView(
|
|
231
|
+
group.getChildAt(index),
|
|
232
|
+
location[0],
|
|
233
|
+
location[1],
|
|
234
|
+
depth + 1,
|
|
235
|
+
false,
|
|
236
|
+
state
|
|
237
|
+
);
|
|
238
|
+
if (child != null) {
|
|
239
|
+
children.pushMap(child);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
node.putArray("children", children);
|
|
244
|
+
return node;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private boolean isReplayMask(View view) {
|
|
248
|
+
Object testId = view.getTag(com.facebook.react.R.id.react_test_id);
|
|
249
|
+
return REPLAY_MASK_TEST_ID.equals(testId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private boolean isOpaqueView(View view) {
|
|
253
|
+
String kind = classify(view, false);
|
|
254
|
+
return "image".equals(kind) || "webview".equals(kind)
|
|
255
|
+
|| "canvas".equals(kind);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private View resolveView(int reactTag) {
|
|
259
|
+
UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(
|
|
260
|
+
getReactApplicationContext(),
|
|
261
|
+
reactTag
|
|
262
|
+
);
|
|
263
|
+
return uiManager == null ? null : uiManager.resolveView(reactTag);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private boolean isReactTag(double value) {
|
|
267
|
+
return Double.isFinite(value) && value > 0 && value <= Integer.MAX_VALUE
|
|
268
|
+
&& value == Math.rint(value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private View findReactTouchRoot(View view) {
|
|
272
|
+
View current = view;
|
|
273
|
+
while (current != null) {
|
|
274
|
+
if (current instanceof RootView) {
|
|
275
|
+
return current;
|
|
276
|
+
}
|
|
277
|
+
android.view.ViewParent parent = current.getParent();
|
|
278
|
+
current = parent instanceof View ? (View) parent : null;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private String classify(View view, boolean masked) {
|
|
284
|
+
if (masked) {
|
|
285
|
+
return "masked";
|
|
286
|
+
}
|
|
287
|
+
if (view instanceof EditText) {
|
|
288
|
+
return "input";
|
|
289
|
+
}
|
|
290
|
+
if (view instanceof Button) {
|
|
291
|
+
return "button";
|
|
292
|
+
}
|
|
293
|
+
if (view instanceof TextView) {
|
|
294
|
+
return "text";
|
|
295
|
+
}
|
|
296
|
+
String className = view.getClass().getName().toLowerCase(Locale.US);
|
|
297
|
+
if (view instanceof ImageView || className.contains("reactimageview")
|
|
298
|
+
|| className.contains("draweeview")) {
|
|
299
|
+
return "image";
|
|
300
|
+
}
|
|
301
|
+
if (view instanceof WebView || view.getClass().getName().contains("WebView")) {
|
|
302
|
+
return "webview";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (view instanceof SurfaceView || view instanceof TextureView
|
|
306
|
+
|| isNamedOpaqueSurface(className)) {
|
|
307
|
+
return "canvas";
|
|
308
|
+
}
|
|
309
|
+
if (view instanceof ScrollView) {
|
|
310
|
+
return "scroll";
|
|
311
|
+
}
|
|
312
|
+
if (!(view instanceof ViewGroup)) {
|
|
313
|
+
/* Unknown leaf views may draw arbitrary private pixels themselves. */
|
|
314
|
+
return view.getClass() == View.class ? "view" : "canvas";
|
|
315
|
+
}
|
|
316
|
+
if (((ViewGroup) view).getChildCount() == 0
|
|
317
|
+
&& !isKnownStructuralContainer(view)) {
|
|
318
|
+
return "canvas";
|
|
319
|
+
}
|
|
320
|
+
return "view";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private boolean isNamedOpaqueSurface(String className) {
|
|
324
|
+
return className.contains("skia") || className.contains("opengl")
|
|
325
|
+
|| className.contains("canvas") || className.contains("svg")
|
|
326
|
+
|| className.contains("mapview") || className.contains("camera")
|
|
327
|
+
|| className.contains("video") || className.contains("player")
|
|
328
|
+
|| className.contains("pdf") || className.contains("signature")
|
|
329
|
+
|| className.contains("drawing") || className.contains("paint")
|
|
330
|
+
|| className.contains("chart");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private boolean isKnownStructuralContainer(View view) {
|
|
334
|
+
if (view instanceof RootView) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
String className = view.getClass().getName();
|
|
338
|
+
if (className.startsWith("android.") || className.startsWith("androidx.")) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
Class<?> current = view.getClass();
|
|
343
|
+
while (current != null) {
|
|
344
|
+
String currentName = current.getName();
|
|
345
|
+
if ("com.facebook.react.views.view.ReactViewGroup".equals(currentName)
|
|
346
|
+
|| "com.facebook.react.ReactRootView".equals(currentName)) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
current = current.getSuperclass();
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private String cssColor(int color) {
|
|
355
|
+
return String.format(
|
|
356
|
+
Locale.US,
|
|
357
|
+
"#%02x%02x%02x%02x",
|
|
358
|
+
Color.red(color),
|
|
359
|
+
Color.green(color),
|
|
360
|
+
Color.blue(color),
|
|
361
|
+
Color.alpha(color)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/*
|
|
366
|
+
* RN 0.81 moved backgrounds and borders out of ColorDrawable. Reflection
|
|
367
|
+
* keeps this library binary-compatible with 0.73-0.80 while using the
|
|
368
|
+
* public 0.81+ BackgroundStyleApplicator getters when they are present.
|
|
369
|
+
* Only visual paint values are requested; no props or content are read.
|
|
370
|
+
*/
|
|
371
|
+
private void putVisualStyles(View view, WritableMap node) {
|
|
372
|
+
Integer backgroundColor = null;
|
|
373
|
+
if (view.getBackground() instanceof ColorDrawable) {
|
|
374
|
+
backgroundColor = ((ColorDrawable) view.getBackground()).getColor();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
Integer legacyBackgroundColor = readLegacyReactBackground(view, node);
|
|
378
|
+
if (legacyBackgroundColor != null) {
|
|
379
|
+
backgroundColor = legacyBackgroundColor;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
Class<?> applicator = Class.forName(
|
|
384
|
+
"com.facebook.react.uimanager.BackgroundStyleApplicator"
|
|
385
|
+
);
|
|
386
|
+
Method getBackgroundColor = applicator.getMethod(
|
|
387
|
+
"getBackgroundColor",
|
|
388
|
+
View.class
|
|
389
|
+
);
|
|
390
|
+
Object reflectedBackground = getBackgroundColor.invoke(null, view);
|
|
391
|
+
if (reflectedBackground instanceof Number) {
|
|
392
|
+
backgroundColor = ((Number) reflectedBackground).intValue();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
Class<?> logicalEdge = Class.forName(
|
|
396
|
+
"com.facebook.react.uimanager.style.LogicalEdge"
|
|
397
|
+
);
|
|
398
|
+
Object allEdges = enumConstant(logicalEdge, "ALL");
|
|
399
|
+
if (allEdges != null) {
|
|
400
|
+
Object reflectedWidth = applicator
|
|
401
|
+
.getMethod("getBorderWidth", View.class, logicalEdge)
|
|
402
|
+
.invoke(null, view, allEdges);
|
|
403
|
+
if (reflectedWidth instanceof Number) {
|
|
404
|
+
double width = ((Number) reflectedWidth).doubleValue();
|
|
405
|
+
if (Double.isFinite(width) && width > 0) {
|
|
406
|
+
node.putDouble("borderWidth", width);
|
|
407
|
+
Object reflectedColor = applicator
|
|
408
|
+
.getMethod("getBorderColor", View.class, logicalEdge)
|
|
409
|
+
.invoke(null, view, allEdges);
|
|
410
|
+
if (reflectedColor instanceof Number) {
|
|
411
|
+
node.putString(
|
|
412
|
+
"borderColor",
|
|
413
|
+
cssColor(((Number) reflectedColor).intValue())
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
Class<?> radiusProperty = Class.forName(
|
|
421
|
+
"com.facebook.react.uimanager.style.BorderRadiusProp"
|
|
422
|
+
);
|
|
423
|
+
Object allCorners = enumConstant(radiusProperty, "BORDER_RADIUS");
|
|
424
|
+
if (allCorners != null) {
|
|
425
|
+
Object radius = applicator
|
|
426
|
+
.getMethod("getBorderRadius", View.class, radiusProperty)
|
|
427
|
+
.invoke(null, view, allCorners);
|
|
428
|
+
if (radius != null) {
|
|
429
|
+
Object reflectedRadius = radius
|
|
430
|
+
.getClass()
|
|
431
|
+
.getMethod("resolve", float.class)
|
|
432
|
+
.invoke(
|
|
433
|
+
radius,
|
|
434
|
+
(float) toDp(
|
|
435
|
+
view,
|
|
436
|
+
Math.min(view.getWidth(), view.getHeight())
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
if (reflectedRadius instanceof Number) {
|
|
440
|
+
double radiusPx = ((Number) reflectedRadius).doubleValue();
|
|
441
|
+
if (Double.isFinite(radiusPx) && radiusPx > 0) {
|
|
442
|
+
node.putDouble("borderRadius", radiusPx);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
|
|
448
|
+
/* RN before the style getter API uses the legacy drawable fallback. */
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (backgroundColor != null) {
|
|
452
|
+
node.putString("backgroundColor", cssColor(backgroundColor));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/*
|
|
457
|
+
* RN 0.73-0.80 stores normal React view paint in
|
|
458
|
+
* ReactViewBackgroundDrawable. Referencing that class directly would make
|
|
459
|
+
* this package fail to load once RN removes it, so the compatibility path
|
|
460
|
+
* checks the exact class name and calls only its public visual getters.
|
|
461
|
+
*/
|
|
462
|
+
private Integer readLegacyReactBackground(View view, WritableMap node) {
|
|
463
|
+
Object background = view.getBackground();
|
|
464
|
+
if (background == null || !(
|
|
465
|
+
"com.facebook.react.views.view.ReactViewBackgroundDrawable"
|
|
466
|
+
).equals(background.getClass().getName())) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
Class<?> legacyDrawable = background.getClass();
|
|
472
|
+
Object reflectedBackground = legacyDrawable
|
|
473
|
+
.getMethod("getColor")
|
|
474
|
+
.invoke(background);
|
|
475
|
+
|
|
476
|
+
Object reflectedWidth = legacyDrawable
|
|
477
|
+
.getMethod("getFullBorderWidth")
|
|
478
|
+
.invoke(background);
|
|
479
|
+
if (reflectedWidth instanceof Number) {
|
|
480
|
+
double width = toDp(
|
|
481
|
+
view,
|
|
482
|
+
((Number) reflectedWidth).doubleValue()
|
|
483
|
+
);
|
|
484
|
+
if (Double.isFinite(width) && width > 0) {
|
|
485
|
+
node.putDouble("borderWidth", width);
|
|
486
|
+
|
|
487
|
+
Class<?> spacing = Class.forName(
|
|
488
|
+
"com.facebook.react.uimanager.Spacing"
|
|
489
|
+
);
|
|
490
|
+
int allEdges = spacing.getField("ALL").getInt(null);
|
|
491
|
+
Object reflectedColor = legacyDrawable
|
|
492
|
+
.getMethod("getBorderColor", int.class)
|
|
493
|
+
.invoke(background, allEdges);
|
|
494
|
+
if (reflectedColor instanceof Number) {
|
|
495
|
+
node.putString(
|
|
496
|
+
"borderColor",
|
|
497
|
+
cssColor(((Number) reflectedColor).intValue())
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
Object reflectedRadius = legacyDrawable
|
|
504
|
+
.getMethod("getFullBorderRadius")
|
|
505
|
+
.invoke(background);
|
|
506
|
+
if (reflectedRadius instanceof Number) {
|
|
507
|
+
double radius = toDp(
|
|
508
|
+
view,
|
|
509
|
+
((Number) reflectedRadius).doubleValue()
|
|
510
|
+
);
|
|
511
|
+
if (Double.isFinite(radius) && radius > 0) {
|
|
512
|
+
node.putDouble("borderRadius", radius);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return reflectedBackground instanceof Number
|
|
517
|
+
? ((Number) reflectedBackground).intValue()
|
|
518
|
+
: null;
|
|
519
|
+
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private Object enumConstant(Class<?> enumClass, String name) {
|
|
525
|
+
Object[] constants = enumClass.getEnumConstants();
|
|
526
|
+
if (constants == null) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
for (Object constant : constants) {
|
|
530
|
+
if (name.equals(constant.toString())) {
|
|
531
|
+
return constant;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private double toDp(View view, double pixels) {
|
|
538
|
+
float density = view.getResources().getDisplayMetrics().density;
|
|
539
|
+
return density > 0 ? pixels / density : pixels;
|
|
540
|
+
}
|
|
541
|
+
}
|