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.
- package/android/build.gradle.kts +21 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/SparklingDebugTool.kt +268 -19
- package/android/src/main/java/com/tiktok/sparkling/debugtool/SparklingDebugToolProviderImpl.kt +93 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLog.kt +128 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLogAdapter.kt +133 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/ConsoleLogStore.kt +81 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleLogDelegate.kt +119 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleLogcatBridge.kt +107 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/console/LynxConsoleSink.kt +66 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/floating/SparklingFloatingBallManager.kt +43 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/GlobalPropsRegistry.kt +59 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/MethodInvocation.kt +52 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/MethodInvocationStore.kt +107 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspect/SparklingDebugAutoWiring.kt +44 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SearchInputSupport.kt +77 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingConsolePanelView.kt +130 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingGlobalPropsPanelView.kt +319 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingInspectorFragment.kt +155 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/inspector/SparklingMethodPanelView.kt +225 -0
- package/android/src/main/java/com/tiktok/sparkling/debugtool/ui/BottomSheetSupport.kt +43 -0
- package/android/src/main/res/color/sparkling_chip_toggle_text.xml +6 -0
- package/android/src/main/res/color/sparkling_inspector_tab_text.xml +5 -0
- package/android/src/main/res/color/sparkling_log_level_tab_text.xml +5 -0
- package/android/src/main/res/drawable/sparkling_chip_bg.xml +6 -0
- package/android/src/main/res/drawable/sparkling_chip_toggle_bg.xml +23 -0
- package/android/src/main/res/drawable/sparkling_flat_tab_bg.xml +25 -0
- package/android/src/main/res/drawable/sparkling_floating_ball_bg.xml +8 -0
- package/android/src/main/res/drawable/sparkling_floating_logo.png +0 -0
- package/android/src/main/res/drawable/sparkling_ic_close.xml +11 -0
- package/android/src/main/res/drawable/sparkling_ic_copy.xml +14 -0
- package/android/src/main/res/drawable/sparkling_inspector_close_bg.xml +9 -0
- package/android/src/main/res/drawable/sparkling_inspector_tab_bg.xml +16 -0
- package/android/src/main/res/drawable/sparkling_log_level_tab_bg.xml +24 -0
- package/android/src/main/res/drawable-night/sparkling_chip_bg.xml +5 -0
- package/android/src/main/res/drawable-night/sparkling_flat_tab_bg.xml +24 -0
- package/android/src/main/res/drawable-night/sparkling_log_level_tab_bg.xml +23 -0
- package/android/src/main/res/layout/fragment_sparkling_inspector.xml +110 -0
- package/android/src/main/res/layout/item_sparkling_console.xml +34 -0
- package/android/src/main/res/layout/item_sparkling_global_props_kv.xml +27 -0
- package/android/src/main/res/layout/item_sparkling_global_props_section.xml +25 -0
- package/android/src/main/res/layout/item_sparkling_method_invocation.xml +74 -0
- package/android/src/main/res/layout/view_sparkling_console_panel.xml +141 -0
- package/android/src/main/res/layout/view_sparkling_global_props_panel.xml +70 -0
- package/android/src/main/res/layout/view_sparkling_method_panel.xml +68 -0
- package/android/src/main/res/values/colors.xml +25 -0
- package/android/src/main/res/values-night/colors.xml +19 -0
- package/android/src/main/resources/META-INF/services/com.tiktok.sparkling.debug.SparklingDebugToolProvider +1 -0
- package/ios/Resources/sparkling_floating_logo.png +0 -0
- package/ios/Resources/sparkling_floating_logo@2x.png +0 -0
- package/ios/Resources/sparkling_floating_logo@3x.png +0 -0
- package/ios/Sources/Console/ConsoleLog.swift +139 -0
- package/ios/Sources/Console/ConsoleLogStore.swift +67 -0
- package/ios/Sources/Console/LynxConsoleSink.swift +50 -0
- package/ios/Sources/Console/SparklingConsoleViewController.swift +459 -0
- package/ios/Sources/Floating/SparklingFloatingBallManager.swift +179 -0
- package/ios/Sources/Inspect/GlobalPropsRegistry.swift +54 -0
- package/ios/Sources/Inspect/MethodInvocation.swift +127 -0
- package/ios/Sources/Inspect/SparklingDebugAutoWiring.swift +231 -0
- package/ios/Sources/Inspect/SparklingGlobalPropsViewController.swift +387 -0
- package/ios/Sources/Inspect/SparklingMethodInvocationViewController.swift +386 -0
- package/ios/Sources/Inspector/SparklingInspectorViewController.swift +296 -0
- package/ios/Sources/SparklingDebugTool.swift +102 -0
- package/ios/Sparkling-DebugTool.podspec +12 -4
- package/module.config.json +1 -0
- package/package.json +1 -1
package/android/build.gradle.kts
CHANGED
|
@@ -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(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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://
|
|
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
|
}
|
package/android/src/main/java/com/tiktok/sparkling/debugtool/SparklingDebugToolProviderImpl.kt
ADDED
|
@@ -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
|
+
}
|