sparkling-debug-tool 2.1.0-rc.24 → 2.1.0-rc.26

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.
Files changed (65) hide show
  1. package/android/build.gradle.kts +21 -0
  2. package/android/src/main/java/com/tiktok/sparkling/debugtool/SparklingDebugTool.kt +268 -19
  3. package/android/src/main/java/com/tiktok/sparkling/debugtool/SparklingDebugToolProviderImpl.kt +93 -0
  4. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLog.kt +128 -0
  5. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLogAdapter.kt +133 -0
  6. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLogStore.kt +81 -0
  7. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleLogDelegate.kt +119 -0
  8. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleLogcatBridge.kt +107 -0
  9. package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleSink.kt +66 -0
  10. package/android/src/main/java/com/tiktok/sparkling/debugtool/floating/SparklingFloatingBallManager.kt +43 -0
  11. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/GlobalPropsRegistry.kt +59 -0
  12. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/MethodInvocation.kt +52 -0
  13. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/MethodInvocationStore.kt +107 -0
  14. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/SparklingDebugAutoWiring.kt +44 -0
  15. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SearchInputSupport.kt +77 -0
  16. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingConsolePanelView.kt +130 -0
  17. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingGlobalPropsPanelView.kt +319 -0
  18. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingInspectorFragment.kt +155 -0
  19. package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingMethodPanelView.kt +225 -0
  20. package/android/src/main/java/com/tiktok/sparkling/debugtool/ui/BottomSheetSupport.kt +43 -0
  21. package/android/src/main/res/color/sparkling_chip_toggle_text.xml +6 -0
  22. package/android/src/main/res/color/sparkling_inspector_tab_text.xml +5 -0
  23. package/android/src/main/res/color/sparkling_log_level_tab_text.xml +5 -0
  24. package/android/src/main/res/drawable/sparkling_chip_bg.xml +6 -0
  25. package/android/src/main/res/drawable/sparkling_chip_toggle_bg.xml +23 -0
  26. package/android/src/main/res/drawable/sparkling_flat_tab_bg.xml +25 -0
  27. package/android/src/main/res/drawable/sparkling_floating_ball_bg.xml +8 -0
  28. package/android/src/main/res/drawable/sparkling_floating_logo.png +0 -0
  29. package/android/src/main/res/drawable/sparkling_ic_close.xml +11 -0
  30. package/android/src/main/res/drawable/sparkling_ic_copy.xml +14 -0
  31. package/android/src/main/res/drawable/sparkling_inspector_close_bg.xml +9 -0
  32. package/android/src/main/res/drawable/sparkling_inspector_tab_bg.xml +16 -0
  33. package/android/src/main/res/drawable/sparkling_log_level_tab_bg.xml +24 -0
  34. package/android/src/main/res/drawable-night/sparkling_chip_bg.xml +5 -0
  35. package/android/src/main/res/drawable-night/sparkling_flat_tab_bg.xml +24 -0
  36. package/android/src/main/res/drawable-night/sparkling_log_level_tab_bg.xml +23 -0
  37. package/android/src/main/res/layout/fragment_sparkling_inspector.xml +110 -0
  38. package/android/src/main/res/layout/item_sparkling_console.xml +34 -0
  39. package/android/src/main/res/layout/item_sparkling_global_props_kv.xml +27 -0
  40. package/android/src/main/res/layout/item_sparkling_global_props_section.xml +25 -0
  41. package/android/src/main/res/layout/item_sparkling_method_invocation.xml +74 -0
  42. package/android/src/main/res/layout/view_sparkling_console_panel.xml +141 -0
  43. package/android/src/main/res/layout/view_sparkling_global_props_panel.xml +70 -0
  44. package/android/src/main/res/layout/view_sparkling_method_panel.xml +68 -0
  45. package/android/src/main/res/values/colors.xml +25 -0
  46. package/android/src/main/res/values-night/colors.xml +19 -0
  47. package/android/src/main/resources/META-INF/services/com.tiktok.sparkling.debug.SparklingDebugToolProvider +1 -0
  48. package/ios/Resources/sparkling_floating_logo.png +0 -0
  49. package/ios/Resources/sparkling_floating_logo@2x.png +0 -0
  50. package/ios/Resources/sparkling_floating_logo@3x.png +0 -0
  51. package/ios/Sources/Console/ConsoleLog.swift +139 -0
  52. package/ios/Sources/Console/ConsoleLogStore.swift +67 -0
  53. package/ios/Sources/Console/LynxConsoleSink.swift +50 -0
  54. package/ios/Sources/Console/SparklingConsoleViewController.swift +459 -0
  55. package/ios/Sources/Floating/SparklingFloatingBallManager.swift +179 -0
  56. package/ios/Sources/Inspect/GlobalPropsRegistry.swift +54 -0
  57. package/ios/Sources/Inspect/MethodInvocation.swift +127 -0
  58. package/ios/Sources/Inspect/SparklingDebugAutoWiring.swift +231 -0
  59. package/ios/Sources/Inspect/SparklingGlobalPropsViewController.swift +387 -0
  60. package/ios/Sources/Inspect/SparklingMethodInvocationViewController.swift +386 -0
  61. package/ios/Sources/Inspector/SparklingInspectorViewController.swift +296 -0
  62. package/ios/Sources/SparklingDebugTool.swift +102 -0
  63. package/ios/Sparkling-DebugTool.podspec +12 -4
  64. package/module.config.json +1 -0
  65. package/package.json +1 -1
@@ -37,10 +37,31 @@ android {
37
37
  }
38
38
 
39
39
  dependencies {
40
+ implementation(libs.androidx.appcompat)
41
+ implementation("androidx.fragment:fragment-ktx:1.6.2")
42
+ implementation("androidx.recyclerview:recyclerview:1.3.2")
43
+ implementation("androidx.cardview:cardview:1.0.0")
40
44
  implementation(libs.lynx)
41
45
  implementation(libs.lynx.service.log)
42
46
  implementation(libs.lynx.service.devtool)
43
47
  implementation(libs.lynx.devtool)
48
+
49
+ val sparklingVersion =
50
+ (findProperty("SPARKLING_ANDROID_SDK_VERSION") as? String)
51
+ ?: System.getenv("SPARKLING_ANDROID_SDK_VERSION")
52
+ ?: "2.1.0-rc.26"
53
+ val localSparkling = rootProject.findProject(":sparkling")
54
+ if (localSparkling != null) {
55
+ compileOnly(localSparkling)
56
+ } else {
57
+ compileOnly("com.tiktok.sparkling:sparkling:$sparklingVersion")
58
+ }
59
+ val localSparklingMethod = rootProject.findProject(":sparkling-method")
60
+ if (localSparklingMethod != null) {
61
+ compileOnly(localSparklingMethod)
62
+ } else {
63
+ compileOnly("com.tiktok.sparkling:sparkling-method:$sparklingVersion")
64
+ }
44
65
  }
45
66
 
46
67
  val publishingGroupId =
@@ -7,32 +7,149 @@ import android.app.Activity
7
7
  import android.app.AlertDialog
8
8
  import android.app.Application
9
9
  import android.content.Context
10
+ import android.content.pm.ApplicationInfo
10
11
  import android.os.Looper
11
12
  import android.text.InputType
12
13
  import android.widget.EditText
13
14
  import android.widget.Toast
15
+ import androidx.fragment.app.DialogFragment
16
+ import androidx.fragment.app.FragmentActivity
14
17
  import com.lynx.devtool.LynxDevtoolEnv
15
18
  import com.lynx.service.devtool.LynxDevToolService
19
+ import com.lynx.service.log.LynxLogService
16
20
  import com.lynx.tasm.LynxEnv
21
+ import com.lynx.tasm.LynxView
17
22
  import com.lynx.tasm.service.LynxServiceCenter
23
+ import com.tiktok.sparkling.debugtool.console.ConsoleLog
24
+ import com.tiktok.sparkling.debugtool.console.ConsoleLogStore
25
+ import com.tiktok.sparkling.debugtool.console.LynxConsoleLogDelegate
26
+ import com.tiktok.sparkling.debugtool.console.LynxConsoleLogcatBridge
27
+ import com.tiktok.sparkling.debugtool.console.LynxConsoleSink
28
+ import com.tiktok.sparkling.debugtool.floating.SparklingFloatingBallManager
29
+ import com.tiktok.sparkling.debugtool.inspect.GlobalPropsRegistry
30
+ import com.tiktok.sparkling.debugtool.inspect.GlobalPropsSnapshot
31
+ import com.tiktok.sparkling.debugtool.inspect.MethodInvocation
32
+ import com.tiktok.sparkling.debugtool.inspect.MethodInvocationStore
33
+ import com.tiktok.sparkling.debugtool.inspect.SparklingDebugAutoWiring
34
+ import com.tiktok.sparkling.debugtool.inspector.SparklingInspectorFragment
18
35
 
36
+ /**
37
+ * Entry point for the Sparkling debug tool. The host typically only needs to
38
+ * call [init] once during `Application.onCreate`; everything else
39
+ * (Lynx debug flags, JS console capture, GlobalProps tracking, Sparkling
40
+ * Method observation, debugTag entry) is wired up automatically when
41
+ * `enableFloatingBall = true`.
42
+ */
19
43
  object SparklingDebugTool {
44
+ data class Config(
45
+ /**
46
+ * When `true` the host app is expected to call [attachConsoleSink] for each
47
+ * created `LynxView` so JS console messages are captured. When the
48
+ * debugTag entry is enabled the debug tool wires this up itself via
49
+ * `KitViewManager`, so this flag is mainly here for explicit override.
50
+ */
51
+ val enableJsConsole: Boolean = false,
52
+ val enableInNonDebuggableApp: Boolean = false,
53
+ /** Maximum number of console messages kept in the in-memory ring buffer. */
54
+ val consoleBufferSize: Int = 300,
55
+ /**
56
+ * When `true` SparklingView shows its bottom-left debugTag entry. Tapping
57
+ * the tag opens the unified inspector; turning this on also auto-enables
58
+ * every Lynx debug capability and wires up the
59
+ * console / globalProps / method tracers.
60
+ */
61
+ val enableFloatingBall: Boolean = false,
62
+ )
63
+
64
+ /**
65
+ * @deprecated Switches are no longer surfaced in the inspector. The tool
66
+ * applies the all-on configuration whenever the debugTag entry is enabled.
67
+ * The class is kept for binary compatibility with hosts that still call
68
+ * [getFlags] / [setFlags] directly.
69
+ */
70
+ data class DebugFlags(
71
+ val lynxDebugEnabled: Boolean = true,
72
+ val devtoolEnabled: Boolean = true,
73
+ val logboxEnabled: Boolean = true,
74
+ val longPressMenuEnabled: Boolean = true,
75
+ )
76
+
20
77
  private const val PREFS_NAME = "sparkling_debug_tool"
21
78
  private const val KEY_DEV_URL = "dev_url"
79
+ private const val KEY_LYNX_DEBUG = "lynx_debug"
80
+ private const val KEY_DEVTOOL = "devtool"
81
+ private const val KEY_LOGBOX = "logbox"
82
+ private const val KEY_LONG_PRESS = "long_press_menu"
83
+
84
+ private var currentConfig = Config()
22
85
 
23
86
  @JvmStatic
24
- fun init(application: Application) {
25
- // Preset values must be set BEFORE LynxEnv flags so the DevTool
26
- // service picks them up during initialization.
27
- LynxDevToolService.INSTANCE.setLynxDebugPresetValue(true)
28
- LynxDevToolService.INSTANCE.setLogBoxPresetValue(true)
29
- LynxDevToolService.INSTANCE.setLoadQJSBridge(true)
87
+ fun init(
88
+ application: Application,
89
+ config: Config = Config(),
90
+ ) {
91
+ currentConfig = config
92
+ ConsoleLogStore.setCapacity(config.consoleBufferSize)
93
+ if (!isDebuggableApp(application) && !config.enableInNonDebuggableApp) {
94
+ return
95
+ }
96
+ // Always apply the all-on debug flag set when running under the debug
97
+ // tool: switches are no longer user-visible. Hosts that need a custom
98
+ // subset can still call [setFlags] explicitly.
99
+ applyFlags(DebugFlags())
100
+ SparklingFloatingBallManager.install(application)
101
+ SparklingFloatingBallManager.setEnabled(config.enableFloatingBall)
102
+ if (config.enableFloatingBall || config.enableJsConsole) {
103
+ LynxConsoleLogDelegate.installOnce()
104
+ LynxConsoleLogcatBridge.startOnce()
105
+ SparklingDebugAutoWiring.installOnce()
106
+ }
107
+ }
30
108
 
31
- LynxServiceCenter.inst().registerService(LynxDevToolService.INSTANCE)
32
- LynxEnv.inst().enableLynxDebug(true)
33
- LynxEnv.inst().enableDevtool(true)
34
- LynxEnv.inst().enableLogBox(true)
35
- LynxDevtoolEnv.inst().enableLongPressMenu(true)
109
+ /** Show or hide the bottom-left debugTag entry at runtime. */
110
+ @JvmStatic
111
+ fun setFloatingBallEnabled(enabled: Boolean) {
112
+ SparklingFloatingBallManager.setEnabled(enabled)
113
+ if (enabled) {
114
+ applyFlags(DebugFlags())
115
+ LynxConsoleLogDelegate.installOnce()
116
+ LynxConsoleLogcatBridge.startOnce()
117
+ SparklingDebugAutoWiring.installOnce()
118
+ }
119
+ }
120
+
121
+ /** Legacy hook kept for compatibility; debugTag clicks use SparklingView's default action. */
122
+ @JvmStatic
123
+ fun setFloatingBallActionHandler(handler: SparklingFloatingBallManager.ActionHandler?) {
124
+ SparklingFloatingBallManager.setActionHandler(handler)
125
+ }
126
+
127
+ /**
128
+ * Build a fresh inspector fragment. Equivalent to invoking the debugTag's
129
+ * default tap action; useful for hosts that want to surface the
130
+ * inspector via a menu / button.
131
+ */
132
+ @JvmStatic
133
+ fun createFragment(): DialogFragment = SparklingInspectorFragment.newInstance()
134
+
135
+ @JvmStatic
136
+ fun getFlags(context: Context): DebugFlags = resolveFlags(context)
137
+
138
+ @JvmStatic
139
+ fun setFlags(
140
+ context: Context,
141
+ flags: DebugFlags,
142
+ ) {
143
+ prefs(context)
144
+ .edit()
145
+ .putBoolean(KEY_LYNX_DEBUG, flags.lynxDebugEnabled)
146
+ .putBoolean(KEY_DEVTOOL, flags.devtoolEnabled)
147
+ .putBoolean(KEY_LOGBOX, flags.logboxEnabled)
148
+ .putBoolean(KEY_LONG_PRESS, flags.longPressMenuEnabled)
149
+ .apply()
150
+ if (isDebuggableApp(context) || currentConfig.enableInNonDebuggableApp) {
151
+ applyFlags(flags)
152
+ }
36
153
  }
37
154
 
38
155
  @JvmStatic
@@ -40,11 +157,7 @@ object SparklingDebugTool {
40
157
  context: Context,
41
158
  fallback: String,
42
159
  ): String {
43
- val stored =
44
- context.applicationContext
45
- .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
46
- .getString(KEY_DEV_URL, null)
47
- ?.trim()
160
+ val stored = prefs(context).getString(KEY_DEV_URL, null)?.trim()
48
161
  return if (stored.isNullOrEmpty()) fallback else stored
49
162
  }
50
163
 
@@ -53,13 +166,121 @@ object SparklingDebugTool {
53
166
  context: Context,
54
167
  url: String,
55
168
  ) {
56
- context.applicationContext
57
- .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
169
+ prefs(context)
58
170
  .edit()
59
171
  .putString(KEY_DEV_URL, url.trim())
60
172
  .apply()
61
173
  }
62
174
 
175
+ @JvmStatic
176
+ fun isJsConsoleEnabled(): Boolean = currentConfig.enableJsConsole
177
+
178
+ /**
179
+ * Hook the Lynx inspector console delegate so that every `console.*` call from
180
+ * the JS context is captured into the shared [ConsoleLogStore]. The debug
181
+ * tool already wires this up automatically via `KitViewManager` when the
182
+ * debugTag entry is enabled; this entry point is kept for hosts that don't
183
+ * use `KitViewManager`.
184
+ *
185
+ * Returns `true` if the delegate was successfully installed.
186
+ */
187
+ @JvmStatic
188
+ fun attachConsoleSink(lynxView: LynxView): Boolean = LynxConsoleSink.attach(lynxView)
189
+
190
+ @JvmStatic
191
+ fun detachConsoleSink(lynxView: LynxView) {
192
+ LynxConsoleSink.detach(lynxView)
193
+ }
194
+
195
+ /** Open the unified inspector dialog with the given initial tab. */
196
+ @JvmStatic
197
+ @JvmOverloads
198
+ fun openInspectorPanel(
199
+ activity: FragmentActivity,
200
+ initialTab: SparklingInspectorFragment.Tab = SparklingInspectorFragment.Tab.CONSOLE,
201
+ ) {
202
+ val fm = activity.supportFragmentManager
203
+ if (fm.findFragmentByTag(SparklingInspectorFragment.FRAGMENT_TAG) != null) return
204
+ SparklingInspectorFragment
205
+ .newInstance(initialTab)
206
+ .show(fm, SparklingInspectorFragment.FRAGMENT_TAG)
207
+ }
208
+
209
+ /** Open the inspector with the Console tab pre-selected. */
210
+ @JvmStatic
211
+ fun openConsolePanel(activity: FragmentActivity) {
212
+ openInspectorPanel(activity, SparklingInspectorFragment.Tab.CONSOLE)
213
+ }
214
+
215
+ /**
216
+ * Open the inspector with the Sparkling Method invocations tab
217
+ * pre-selected.
218
+ */
219
+ @JvmStatic
220
+ fun openMethodInvocationPanel(activity: FragmentActivity) {
221
+ openInspectorPanel(activity, SparklingInspectorFragment.Tab.METHODS)
222
+ }
223
+
224
+ /** Open the inspector with the GlobalProps tab pre-selected. */
225
+ @JvmStatic
226
+ fun openGlobalPropsPanel(activity: FragmentActivity) {
227
+ openInspectorPanel(activity, SparklingInspectorFragment.Tab.GLOBAL_PROPS)
228
+ }
229
+
230
+ /** Append one method invocation record (already-formatted strings). */
231
+ @JvmStatic
232
+ fun recordMethodInvocation(record: MethodInvocation) {
233
+ MethodInvocationStore.add(record)
234
+ }
235
+
236
+ /** Replace an in-flight invocation record (matched by id). */
237
+ @JvmStatic
238
+ fun updateMethodInvocation(record: MethodInvocation) {
239
+ MethodInvocationStore.update(record)
240
+ }
241
+
242
+ @JvmStatic
243
+ fun clearMethodInvocations() {
244
+ MethodInvocationStore.clear()
245
+ }
246
+
247
+ /**
248
+ * Override the GlobalProps supplier. The debug tool registers a default
249
+ * provider that walks `KitViewManager` reflectively when `init` is called
250
+ * with `enableFloatingBall = true`; hosts only need to call this if they
251
+ * want to expose a different data source.
252
+ */
253
+ @JvmStatic
254
+ fun setGlobalPropsProvider(provider: GlobalPropsRegistry.Provider?) {
255
+ GlobalPropsRegistry.setProvider(provider)
256
+ }
257
+
258
+ /**
259
+ * Push one externally-collected console line (e.g. from the host
260
+ * `HybridLogger`) into the shared store. The native panel will display it
261
+ * alongside JS console messages captured via [attachConsoleSink].
262
+ */
263
+ @JvmStatic
264
+ fun recordConsoleLine(
265
+ level: String,
266
+ tag: String,
267
+ message: String,
268
+ ) {
269
+ ConsoleLogStore.add(ConsoleLog.fromPlain(level, tag, message))
270
+ }
271
+
272
+ /**
273
+ * Snapshot helper kept for backwards compatibility with previous tooling that
274
+ * rendered the console as a single text dump.
275
+ */
276
+ @JvmStatic
277
+ fun getConsoleLines(): List<String> = ConsoleLogStore.snapshot().map { "[${it.type}] ${if (it.tag.isEmpty()) "" else it.tag + ": "}${it.message}" }
278
+
279
+ @JvmStatic
280
+ fun clearConsoleLines() {
281
+ ConsoleLogStore.clear()
282
+ }
283
+
63
284
  @JvmStatic
64
285
  fun showDevUrlDialog(
65
286
  activity: Activity,
@@ -74,7 +295,7 @@ object SparklingDebugTool {
74
295
  val input =
75
296
  EditText(activity).apply {
76
297
  setText(initialUrl ?: "")
77
- hint = "http://10.0.2.2:5969/main.lynx.bundle"
298
+ hint = "http://127.0.0.1:5969/main.lynx.bundle"
78
299
  inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
79
300
  setSelection(text.length)
80
301
  }
@@ -109,4 +330,32 @@ object SparklingDebugTool {
109
330
 
110
331
  dialog.show()
111
332
  }
333
+
334
+ private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
335
+
336
+ private fun isDebuggableApp(context: Context): Boolean = (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
337
+
338
+ private fun resolveFlags(context: Context): DebugFlags {
339
+ val prefs = prefs(context)
340
+ return DebugFlags(
341
+ lynxDebugEnabled = prefs.getBoolean(KEY_LYNX_DEBUG, true),
342
+ devtoolEnabled = prefs.getBoolean(KEY_DEVTOOL, true),
343
+ logboxEnabled = prefs.getBoolean(KEY_LOGBOX, true),
344
+ longPressMenuEnabled = prefs.getBoolean(KEY_LONG_PRESS, true),
345
+ )
346
+ }
347
+
348
+ private fun applyFlags(flags: DebugFlags) {
349
+ LynxDevToolService.INSTANCE.setLynxDebugPresetValue(flags.lynxDebugEnabled)
350
+ LynxDevToolService.INSTANCE.setLogBoxPresetValue(flags.logboxEnabled)
351
+ LynxDevToolService.INSTANCE.setLoadQJSBridge(flags.devtoolEnabled)
352
+
353
+ LynxServiceCenter.inst().registerService(LynxLogService)
354
+ LynxServiceCenter.inst().registerService(LynxDevToolService.INSTANCE)
355
+
356
+ LynxEnv.inst().enableLynxDebug(flags.lynxDebugEnabled)
357
+ LynxEnv.inst().enableDevtool(flags.devtoolEnabled)
358
+ LynxEnv.inst().enableLogBox(flags.logboxEnabled)
359
+ LynxDevtoolEnv.inst().enableLongPressMenu(flags.longPressMenuEnabled)
360
+ }
112
361
  }
@@ -0,0 +1,93 @@
1
+ // Copyright (c) 2026 TikTok Pte. Ltd.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+ package com.tiktok.sparkling.debugtool
5
+
6
+ import android.net.Uri
7
+ import androidx.fragment.app.FragmentActivity
8
+ import com.lynx.tasm.LynxView
9
+ import com.tiktok.sparkling.debug.SparklingDebugToolProvider
10
+ import com.tiktok.sparkling.debugtool.console.LynxConsoleSink
11
+ import com.tiktok.sparkling.debugtool.inspect.GlobalPropsRegistry
12
+ import com.tiktok.sparkling.debugtool.inspect.GlobalPropsSnapshot
13
+ import com.tiktok.sparkling.debugtool.inspect.MethodInvocation
14
+ import com.tiktok.sparkling.debugtool.inspect.MethodInvocationStore
15
+ import com.tiktok.sparkling.debugtool.inspect.SparklingDebugAutoWiring
16
+ import com.tiktok.sparkling.hybridkit.base.IKitView
17
+ import com.tiktok.sparkling.hybridkit.config.RuntimeInfo
18
+ import com.tiktok.sparkling.method.registry.api.SparklingMethodInvocationCenter
19
+
20
+ class SparklingDebugToolProviderImpl : SparklingDebugToolProvider {
21
+ override fun openInspectorPanel(activity: FragmentActivity) {
22
+ SparklingDebugTool.openInspectorPanel(activity)
23
+ }
24
+
25
+ override fun onKitViewCreated(
26
+ containerId: String,
27
+ kitView: IKitView,
28
+ ) {
29
+ SparklingDebugAutoWiring.attachConsoleWithRetry(kitView.realView())
30
+ GlobalPropsRegistry.notifyChanged()
31
+ }
32
+
33
+ override fun onKitViewDestroyed(
34
+ containerId: String,
35
+ kitView: IKitView?,
36
+ ) {
37
+ (kitView?.realView() as? LynxView)?.let { LynxConsoleSink.detach(it) }
38
+ GlobalPropsRegistry.notifyChanged()
39
+ }
40
+
41
+ override fun onMethodInvocationStart(event: SparklingMethodInvocationCenter.Event) {
42
+ MethodInvocationStore.add(event.toMethodInvocation(isEnd = false))
43
+ }
44
+
45
+ override fun onMethodInvocationEnd(event: SparklingMethodInvocationCenter.Event) {
46
+ MethodInvocationStore.update(event.toMethodInvocation(isEnd = true))
47
+ }
48
+
49
+ override fun setGlobalPropsCollector(collector: () -> Map<String, IKitView>) {
50
+ GlobalPropsRegistry.setProvider {
51
+ collector().map { (containerId, kitView) ->
52
+ val raw = kitView.getGlobalProps().orEmpty()
53
+ val templateUrl = kitView.getScheme()
54
+
55
+ @Suppress("UNCHECKED_CAST")
56
+ val queryItems =
57
+ (raw[RuntimeInfo.QUERY_ITEMS] as? Map<String, Any?>)
58
+ ?.takeIf { it.isNotEmpty() }
59
+ ?: parseQueryItems(templateUrl)
60
+ GlobalPropsSnapshot(
61
+ containerId = containerId,
62
+ templateUrl = templateUrl,
63
+ globalProps = raw.filterKeys { it != RuntimeInfo.QUERY_ITEMS },
64
+ queryItems = queryItems,
65
+ )
66
+ }
67
+ }
68
+ }
69
+
70
+ private fun SparklingMethodInvocationCenter.Event.toMethodInvocation(isEnd: Boolean): MethodInvocation {
71
+ val code = code
72
+ return MethodInvocation(
73
+ id = id,
74
+ name = name,
75
+ namespace = namespace,
76
+ platform = platform.toString(),
77
+ params = MethodInvocation.formatPayload(params),
78
+ result = if (isEnd) MethodInvocation.formatPayload(result) else null,
79
+ code = code,
80
+ success = if (isEnd) code == null || code == 1 else null,
81
+ startTimeMs = startTimeMs,
82
+ endTimeMs = endTimeMs,
83
+ )
84
+ }
85
+
86
+ private fun parseQueryItems(url: String?): Map<String, Any?> {
87
+ if (url.isNullOrBlank()) return emptyMap()
88
+ return runCatching {
89
+ val uri = Uri.parse(url)
90
+ uri.queryParameterNames.associateWith { uri.getQueryParameter(it) }
91
+ }.getOrDefault(emptyMap())
92
+ }
93
+ }
@@ -0,0 +1,128 @@
1
+ // Copyright (c) 2026 TikTok Pte. Ltd.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+ package com.tiktok.sparkling.debugtool.console
5
+
6
+ import android.util.Log
7
+ import org.json.JSONArray
8
+ import org.json.JSONObject
9
+
10
+ /**
11
+ * One JS console message captured from a Lynx view via the inspector console
12
+ * delegate, or from the legacy [com.tiktok.sparkling.debugtool.SparklingDebugTool.recordConsoleLine]
13
+ * bridge.
14
+ *
15
+ * The Lynx `LynxInspectorConsoleDelegate` emits a JSON payload of the form
16
+ * `{"type":"log|debug|info|warn|error","data":[ ... ]}`. We flatten the data
17
+ * array into a single human-readable string while retaining the level so the
18
+ * UI can color/filter by it.
19
+ */
20
+ data class ConsoleLog(
21
+ val type: String,
22
+ val level: Int,
23
+ val message: String,
24
+ val tag: String = "",
25
+ val timestamp: Long = System.currentTimeMillis(),
26
+ var expanded: Boolean = false,
27
+ ) {
28
+ fun matches(keyword: String): Boolean {
29
+ if (keyword.isEmpty()) return true
30
+ val needle = keyword.trim()
31
+ if (needle.isEmpty()) return true
32
+ return message.contains(needle, ignoreCase = true) ||
33
+ tag.contains(needle, ignoreCase = true) ||
34
+ type.contains(needle, ignoreCase = true)
35
+ }
36
+
37
+ companion object {
38
+ const val TYPE_LOG = "log"
39
+ const val TYPE_DEBUG = "debug"
40
+ const val TYPE_INFO = "info"
41
+ const val TYPE_WARN = "warn"
42
+ const val TYPE_ERROR = "error"
43
+
44
+ fun levelFromType(type: String?): Int =
45
+ when (type?.lowercase()) {
46
+ TYPE_DEBUG -> Log.DEBUG
47
+ TYPE_INFO -> Log.INFO
48
+ TYPE_WARN -> Log.WARN
49
+ TYPE_ERROR -> Log.ERROR
50
+ TYPE_LOG -> Log.VERBOSE
51
+ else -> Log.VERBOSE
52
+ }
53
+
54
+ fun levelFromName(name: String?): Int =
55
+ when (name?.uppercase()) {
56
+ "V", "VERBOSE" -> Log.VERBOSE
57
+ "D", "DEBUG" -> Log.DEBUG
58
+ "I", "INFO" -> Log.INFO
59
+ "W", "WARN", "WARNING" -> Log.WARN
60
+ "E", "ERROR" -> Log.ERROR
61
+ else -> Log.INFO
62
+ }
63
+
64
+ fun typeFromLevel(level: Int): String =
65
+ when (level) {
66
+ Log.VERBOSE -> TYPE_LOG
67
+ Log.DEBUG -> TYPE_DEBUG
68
+ Log.INFO -> TYPE_INFO
69
+ Log.WARN -> TYPE_WARN
70
+ Log.ERROR -> TYPE_ERROR
71
+ else -> TYPE_LOG
72
+ }
73
+
74
+ /** Parse one Lynx inspector console message JSON payload. */
75
+ fun fromJson(raw: String): ConsoleLog? {
76
+ if (raw.isBlank()) return null
77
+ return try {
78
+ val obj = JSONObject(raw)
79
+ val type = obj.optString("type", TYPE_LOG)
80
+ val data = obj.optJSONArray("data")
81
+ val text = formatData(data) ?: obj.optString("message", raw)
82
+ ConsoleLog(
83
+ type = type,
84
+ level = levelFromType(type),
85
+ message = text,
86
+ )
87
+ } catch (t: Throwable) {
88
+ ConsoleLog(
89
+ type = TYPE_LOG,
90
+ level = Log.VERBOSE,
91
+ message = raw,
92
+ )
93
+ }
94
+ }
95
+
96
+ /** Build a plain console line that didn't come through the JSON inspector pipe. */
97
+ fun fromPlain(
98
+ level: String,
99
+ tag: String,
100
+ message: String,
101
+ ): ConsoleLog {
102
+ val lvl = levelFromName(level)
103
+ return ConsoleLog(
104
+ type = typeFromLevel(lvl),
105
+ level = lvl,
106
+ message = message,
107
+ tag = tag,
108
+ )
109
+ }
110
+
111
+ private fun formatData(data: JSONArray?): String? {
112
+ if (data == null) return null
113
+ val length = data.length()
114
+ if (length == 0) return ""
115
+ val builder = StringBuilder()
116
+ for (i in 0 until length) {
117
+ if (i > 0) builder.append('\t')
118
+ when (val item = data.opt(i)) {
119
+ null, JSONObject.NULL -> builder.append("null")
120
+ is JSONObject -> builder.append(item.toString(2))
121
+ is JSONArray -> builder.append(item.toString(2))
122
+ else -> builder.append(item.toString())
123
+ }
124
+ }
125
+ return builder.toString()
126
+ }
127
+ }
128
+ }