react-native-inapp-inspector 1.1.35 → 2.0.0
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/README.md +123 -101
- package/android/build.gradle +13 -1
- package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +904 -0
- package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.kt +17 -0
- package/dist/commonjs/components/CopyButton.js +3 -0
- package/dist/commonjs/components/ErrorBoundary.js +3 -0
- package/dist/commonjs/components/Inspector/AnalyticsTab.js +7 -3
- package/dist/commonjs/components/Inspector/BundleTab.js +1 -1
- package/dist/commonjs/components/Inspector/ConsoleTab.js +1 -1
- package/dist/commonjs/components/Inspector/CrashTab.js +97 -90
- package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -8
- package/dist/commonjs/components/Inspector/MainScreen.js +14 -101
- package/dist/commonjs/components/Inspector/NetworkTab.js +47 -10
- package/dist/commonjs/components/Inspector/PerformanceTab.js +96 -3
- package/dist/commonjs/components/Inspector/ReduxTab.js +18 -10
- package/dist/commonjs/components/Inspector/SettingsPanel.js +342 -88
- package/dist/commonjs/components/Inspector/TabBar.js +6 -3
- package/dist/commonjs/components/NetworkIcons.d.ts +1 -0
- package/dist/commonjs/components/NetworkIcons.js +6 -1
- package/dist/commonjs/components/TouchableScale.d.ts +1 -0
- package/dist/commonjs/components/TouchableScale.js +30 -32
- package/dist/commonjs/constants/index.d.ts +1 -2
- package/dist/commonjs/constants/index.js +4 -0
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/customHooks/analyticsLogger.d.ts +4 -0
- package/dist/commonjs/customHooks/analyticsLogger.js +7 -5
- package/dist/commonjs/customHooks/crashHandler.js +7 -0
- package/dist/commonjs/helpers/index.js +10 -1
- package/dist/commonjs/helpers/settingsStore.d.ts +9 -0
- package/dist/commonjs/helpers/settingsStore.js +59 -3
- package/dist/commonjs/index.d.ts +1 -0
- package/dist/commonjs/index.js +229 -39
- package/dist/commonjs/native/NativeInspector.d.ts +125 -0
- package/dist/commonjs/native/NativeInspector.js +313 -0
- package/dist/commonjs/native/NativeNetworkInspector.d.ts +22 -0
- package/dist/commonjs/native/NativeNetworkInspector.js +4 -0
- package/dist/commonjs/styles/index.js +6 -6
- package/dist/commonjs/types/enums.d.ts +1 -1
- package/dist/commonjs/types/interfaces.d.ts +9 -0
- package/dist/esm/components/CopyButton.js +3 -0
- package/dist/esm/components/ErrorBoundary.js +3 -0
- package/dist/esm/components/Inspector/AnalyticsTab.js +7 -3
- package/dist/esm/components/Inspector/BundleTab.js +1 -1
- package/dist/esm/components/Inspector/ConsoleTab.js +1 -1
- package/dist/esm/components/Inspector/CrashTab.js +97 -90
- package/dist/esm/components/Inspector/InspectorHeader.js +13 -9
- package/dist/esm/components/Inspector/MainScreen.js +15 -69
- package/dist/esm/components/Inspector/NetworkTab.js +47 -10
- package/dist/esm/components/Inspector/PerformanceTab.js +96 -3
- package/dist/esm/components/Inspector/ReduxTab.js +18 -10
- package/dist/esm/components/Inspector/SettingsPanel.js +344 -90
- package/dist/esm/components/Inspector/TabBar.js +7 -4
- package/dist/esm/components/NetworkIcons.d.ts +1 -0
- package/dist/esm/components/NetworkIcons.js +4 -0
- package/dist/esm/components/TouchableScale.d.ts +1 -0
- package/dist/esm/components/TouchableScale.js +31 -33
- package/dist/esm/constants/index.d.ts +1 -2
- package/dist/esm/constants/index.js +4 -0
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/customHooks/analyticsLogger.d.ts +4 -0
- package/dist/esm/customHooks/analyticsLogger.js +5 -4
- package/dist/esm/customHooks/crashHandler.js +7 -0
- package/dist/esm/helpers/index.js +10 -1
- package/dist/esm/helpers/settingsStore.d.ts +9 -0
- package/dist/esm/helpers/settingsStore.js +58 -3
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +215 -39
- package/dist/esm/native/NativeInspector.d.ts +125 -0
- package/dist/esm/native/NativeInspector.js +289 -0
- package/dist/esm/native/NativeNetworkInspector.d.ts +22 -0
- package/dist/esm/native/NativeNetworkInspector.js +2 -0
- package/dist/esm/styles/index.js +6 -6
- package/dist/esm/types/enums.d.ts +1 -1
- package/dist/esm/types/interfaces.d.ts +9 -0
- package/ios/NetworkInspectorModule.h +6 -0
- package/ios/NetworkInspectorModule.m +720 -5
- package/package.json +13 -2
- package/react-native-inapp-inspector.podspec +7 -2
- package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +0 -97
- package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.java +0 -28
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
package com.inappinspector
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.os.BatteryManager
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.os.Debug
|
|
10
|
+
import android.os.Environment
|
|
11
|
+
import android.os.StatFs
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import com.facebook.react.bridge.Arguments
|
|
14
|
+
import com.facebook.react.bridge.Promise
|
|
15
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
16
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
17
|
+
import com.facebook.react.bridge.ReactMethod
|
|
18
|
+
import com.facebook.react.bridge.WritableMap
|
|
19
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
20
|
+
import java.io.PrintWriter
|
|
21
|
+
import java.io.StringWriter
|
|
22
|
+
import java.util.concurrent.Executors
|
|
23
|
+
|
|
24
|
+
class NetworkInspectorModule(private val reactContext: ReactApplicationContext) :
|
|
25
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
const val MODULE_NAME = "NetworkInspectorModule"
|
|
29
|
+
private const val TAG = "NetworkInspector"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private val networkExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-NetworkWorker") }
|
|
33
|
+
private val consoleExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-ConsoleWorker") }
|
|
34
|
+
private val analyticsExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-AnalyticsWorker") }
|
|
35
|
+
private val reduxExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-ReduxWorker") }
|
|
36
|
+
private val crashExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-CrashWorker") }
|
|
37
|
+
private val metricsExecutor = Executors.newSingleThreadExecutor { Thread(it, "InAppInspector-MetricsWorker") }
|
|
38
|
+
|
|
39
|
+
private var defaultHandler: Thread.UncaughtExceptionHandler? = null
|
|
40
|
+
private var isProtectionEnabled = false
|
|
41
|
+
|
|
42
|
+
override fun getName(): String {
|
|
43
|
+
return MODULE_NAME
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun setupNativeCrashProtection() {
|
|
47
|
+
if (isProtectionEnabled) return
|
|
48
|
+
|
|
49
|
+
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
50
|
+
|
|
51
|
+
// 1. Intercept all uncaught exceptions on background/worker threads (prevents OS force-close)
|
|
52
|
+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
53
|
+
try {
|
|
54
|
+
handleNativeCrash(thread, throwable)
|
|
55
|
+
} catch (e: Exception) {
|
|
56
|
+
Log.e(TAG, "Error in native crash handler", e)
|
|
57
|
+
}
|
|
58
|
+
// Suppress defaultHandler?.uncaughtException() to prevent OS killing the process
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Main Looper Crash Shield: Protects UI thread from dying on uncaught exceptions
|
|
62
|
+
try {
|
|
63
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
android.os.Looper.loop()
|
|
67
|
+
} catch (e: Throwable) {
|
|
68
|
+
try {
|
|
69
|
+
handleNativeCrash(Thread.currentThread(), e)
|
|
70
|
+
} catch (ex: Exception) {
|
|
71
|
+
Log.e(TAG, "Error in Main Looper crash protection", ex)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
Log.w(TAG, "Failed to start Looper crash protection", e)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
isProtectionEnabled = true
|
|
81
|
+
Log.i(TAG, "Native Android Crash Shield enabled (Process kill prevention active)")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private fun handleNativeCrash(thread: Thread, throwable: Throwable) {
|
|
85
|
+
try {
|
|
86
|
+
val sw = StringWriter()
|
|
87
|
+
val pw = PrintWriter(sw)
|
|
88
|
+
throwable.printStackTrace(pw)
|
|
89
|
+
val stackTrace = sw.toString()
|
|
90
|
+
|
|
91
|
+
val params = Arguments.createMap().apply {
|
|
92
|
+
putString("platform", "android")
|
|
93
|
+
putString("error", throwable.message ?: "Unknown native exception")
|
|
94
|
+
putString("name", throwable.javaClass.name)
|
|
95
|
+
putString("stack", stackTrace)
|
|
96
|
+
putString("threadName", thread.name)
|
|
97
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (reactContext.hasActiveReactInstance()) {
|
|
101
|
+
reactContext
|
|
102
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
103
|
+
.emit("onNativeCrash", params)
|
|
104
|
+
}
|
|
105
|
+
} catch (e: Exception) {
|
|
106
|
+
Log.e(TAG, "Failed to emit onNativeCrash to JS", e)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@ReactMethod
|
|
111
|
+
fun enableNativeCrashProtection(promise: Promise) {
|
|
112
|
+
try {
|
|
113
|
+
setupNativeCrashProtection()
|
|
114
|
+
promise.resolve(true)
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
promise.reject("CRASH_PROTECTION_ERROR", e.message, e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@ReactMethod
|
|
121
|
+
fun getDeviceMetrics(promise: Promise) {
|
|
122
|
+
try {
|
|
123
|
+
val map = Arguments.createMap()
|
|
124
|
+
|
|
125
|
+
// 1. RAM / Memory Metrics
|
|
126
|
+
val actManager = reactContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
|
|
127
|
+
if (actManager != null) {
|
|
128
|
+
val memInfo = ActivityManager.MemoryInfo()
|
|
129
|
+
actManager.getMemoryInfo(memInfo)
|
|
130
|
+
map.putDouble("totalRAM", memInfo.totalMem.toDouble())
|
|
131
|
+
map.putDouble("freeRAM", memInfo.availMem.toDouble())
|
|
132
|
+
map.putDouble("usedRAM", (memInfo.totalMem - memInfo.availMem).toDouble())
|
|
133
|
+
map.putBoolean("isLowMemory", memInfo.lowMemory)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Native Heap Allocation
|
|
137
|
+
map.putDouble("nativeHeapAllocated", Debug.getNativeHeapAllocatedSize().toDouble())
|
|
138
|
+
map.putDouble("nativeHeapSize", Debug.getNativeHeapSize().toDouble())
|
|
139
|
+
map.putDouble("nativeHeapFree", Debug.getNativeHeapFreeSize().toDouble())
|
|
140
|
+
|
|
141
|
+
// 3. Storage Space
|
|
142
|
+
try {
|
|
143
|
+
val statFs = StatFs(Environment.getDataDirectory().path)
|
|
144
|
+
val freeStorage = statFs.availableBlocksLong * statFs.blockSizeLong
|
|
145
|
+
val totalStorage = statFs.blockCountLong * statFs.blockSizeLong
|
|
146
|
+
map.putDouble("freeStorage", freeStorage.toDouble())
|
|
147
|
+
map.putDouble("totalStorage", totalStorage.toDouble())
|
|
148
|
+
} catch (e: Exception) {
|
|
149
|
+
// Storage stat optional fallback
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4. Battery Level & State
|
|
153
|
+
try {
|
|
154
|
+
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
|
155
|
+
val batteryStatus = reactContext.registerReceiver(null, ifilter)
|
|
156
|
+
if (batteryStatus != null) {
|
|
157
|
+
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
|
158
|
+
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
|
159
|
+
if (level >= 0 && scale > 0) {
|
|
160
|
+
map.putDouble("batteryPercent", (level * 100.0 / scale))
|
|
161
|
+
}
|
|
162
|
+
val status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
|
163
|
+
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
|
164
|
+
status == BatteryManager.BATTERY_STATUS_FULL
|
|
165
|
+
map.putBoolean("isCharging", isCharging)
|
|
166
|
+
}
|
|
167
|
+
} catch (e: Exception) {
|
|
168
|
+
// Battery status optional fallback
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 5. Hardware & OS Identifiers
|
|
172
|
+
map.putString("deviceModel", Build.MODEL)
|
|
173
|
+
map.putString("deviceBrand", Build.MANUFACTURER)
|
|
174
|
+
map.putString("osVersion", Build.VERSION.RELEASE)
|
|
175
|
+
map.putInt("apiLevel", Build.VERSION.SDK_INT)
|
|
176
|
+
val cpuAbi = if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown"
|
|
177
|
+
map.putString("cpuAbi", cpuAbi)
|
|
178
|
+
|
|
179
|
+
promise.resolve(map)
|
|
180
|
+
} catch (e: Exception) {
|
|
181
|
+
promise.reject("DEVICE_METRICS_ERROR", e.message, e)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private var floatingButton: InAppInspectorFloatingView? = null
|
|
186
|
+
|
|
187
|
+
private fun emitFloatingButtonPress() {
|
|
188
|
+
if (reactContext.hasActiveReactInstance()) {
|
|
189
|
+
reactContext
|
|
190
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
191
|
+
.emit("onFloatingButtonPress", null)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@ReactMethod
|
|
196
|
+
fun showFloatingButton(options: com.facebook.react.bridge.ReadableMap?, promise: Promise) {
|
|
197
|
+
val activity = currentActivity ?: run {
|
|
198
|
+
promise.resolve(false)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
activity.runOnUiThread {
|
|
203
|
+
try {
|
|
204
|
+
val decorView = activity.window.decorView as? android.view.ViewGroup
|
|
205
|
+
if (decorView == null) {
|
|
206
|
+
promise.resolve(false)
|
|
207
|
+
return@runOnUiThread
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
val density = activity.resources.displayMetrics.density
|
|
211
|
+
val sizeDp = if (options != null && options.hasKey("size")) options.getDouble("size").toFloat() else 64f
|
|
212
|
+
val sizePx = (sizeDp * density).toInt()
|
|
213
|
+
|
|
214
|
+
if (floatingButton == null) {
|
|
215
|
+
floatingButton = InAppInspectorFloatingView(activity) {
|
|
216
|
+
emitFloatingButtonPress()
|
|
217
|
+
}
|
|
218
|
+
val params = android.widget.FrameLayout.LayoutParams(sizePx, sizePx)
|
|
219
|
+
val initialX = if (options != null && options.hasKey("x")) {
|
|
220
|
+
(options.getDouble("x") * density).toFloat()
|
|
221
|
+
} else {
|
|
222
|
+
(decorView.width - sizePx - (20 * density)).coerceAtLeast(20 * density)
|
|
223
|
+
}
|
|
224
|
+
val initialY = if (options != null && options.hasKey("y")) {
|
|
225
|
+
(options.getDouble("y") * density).toFloat()
|
|
226
|
+
} else {
|
|
227
|
+
(decorView.height - sizePx - (110 * density)).coerceAtLeast(110 * density)
|
|
228
|
+
}
|
|
229
|
+
floatingButton?.x = initialX
|
|
230
|
+
floatingButton?.y = initialY
|
|
231
|
+
decorView.addView(floatingButton, params)
|
|
232
|
+
} else {
|
|
233
|
+
if (floatingButton?.parent == null) {
|
|
234
|
+
val params = android.widget.FrameLayout.LayoutParams(sizePx, sizePx)
|
|
235
|
+
decorView.addView(floatingButton, params)
|
|
236
|
+
} else {
|
|
237
|
+
decorView.bringChildToFront(floatingButton)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
floatingButton?.visibility = android.view.View.VISIBLE
|
|
242
|
+
floatingButton?.alpha = 0f
|
|
243
|
+
floatingButton?.animate()?.alpha(1f)?.setDuration(200)?.start()
|
|
244
|
+
|
|
245
|
+
promise.resolve(true)
|
|
246
|
+
} catch (e: Exception) {
|
|
247
|
+
Log.e(TAG, "Error showing floating button", e)
|
|
248
|
+
promise.reject("FLOATING_BTN_ERROR", e.message, e)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@ReactMethod
|
|
254
|
+
fun hideFloatingButton(promise: Promise) {
|
|
255
|
+
val activity = currentActivity ?: run {
|
|
256
|
+
promise.resolve(false)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
activity.runOnUiThread {
|
|
261
|
+
try {
|
|
262
|
+
floatingButton?.animate()?.alpha(0f)?.setDuration(150)?.withEndAction {
|
|
263
|
+
floatingButton?.visibility = android.view.View.GONE
|
|
264
|
+
}?.start()
|
|
265
|
+
promise.resolve(true)
|
|
266
|
+
} catch (e: Exception) {
|
|
267
|
+
promise.reject("FLOATING_BTN_ERROR", e.message, e)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@ReactMethod
|
|
273
|
+
fun setFloatingButtonBadge(hasBadge: Boolean, promise: Promise) {
|
|
274
|
+
val activity = currentActivity ?: run {
|
|
275
|
+
promise.resolve(false)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
activity.runOnUiThread {
|
|
280
|
+
floatingButton?.setBadgeVisible(hasBadge)
|
|
281
|
+
promise.resolve(true)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@ReactMethod
|
|
286
|
+
fun setFloatingButtonPosition(x: Double, y: Double, promise: Promise) {
|
|
287
|
+
val activity = currentActivity ?: run {
|
|
288
|
+
promise.resolve(false)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
activity.runOnUiThread {
|
|
293
|
+
val density = activity.resources.displayMetrics.density
|
|
294
|
+
floatingButton?.x = (x * density).toFloat()
|
|
295
|
+
floatingButton?.y = (y * density).toFloat()
|
|
296
|
+
promise.resolve(true)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private var sensorManager: android.hardware.SensorManager? = null
|
|
301
|
+
private var lastShakeTime = 0L
|
|
302
|
+
private var isFpsMonitoring = false
|
|
303
|
+
private var lastFrameTimeNanos = 0L
|
|
304
|
+
private var frameCount = 0
|
|
305
|
+
private var currentCalculatedFps = 60.0
|
|
306
|
+
|
|
307
|
+
private val frameCallback = object : android.view.Choreographer.FrameCallback {
|
|
308
|
+
override fun doFrame(frameTimeNanos: Long) {
|
|
309
|
+
if (!isFpsMonitoring) return
|
|
310
|
+
if (lastFrameTimeNanos == 0L) {
|
|
311
|
+
lastFrameTimeNanos = frameTimeNanos
|
|
312
|
+
} else {
|
|
313
|
+
frameCount++
|
|
314
|
+
val elapsedNanos = frameTimeNanos - lastFrameTimeNanos
|
|
315
|
+
if (elapsedNanos >= 500_000_000L) {
|
|
316
|
+
currentCalculatedFps = (frameCount * 1_000_000_000.0) / elapsedNanos
|
|
317
|
+
frameCount = 0
|
|
318
|
+
lastFrameTimeNanos = frameTimeNanos
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
android.view.Choreographer.getInstance().postFrameCallback(this)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private val shakeListener = object : android.hardware.SensorEventListener {
|
|
326
|
+
override fun onSensorChanged(event: android.hardware.SensorEvent) {
|
|
327
|
+
val x = event.values[0]
|
|
328
|
+
val y = event.values[1]
|
|
329
|
+
val z = event.values[2]
|
|
330
|
+
val gX = x / android.hardware.SensorManager.GRAVITY_EARTH
|
|
331
|
+
val gY = y / android.hardware.SensorManager.GRAVITY_EARTH
|
|
332
|
+
val gZ = z / android.hardware.SensorManager.GRAVITY_EARTH
|
|
333
|
+
val gForce = Math.sqrt((gX * gX + gY * gY + gZ * gZ).toDouble()).toFloat()
|
|
334
|
+
if (gForce > 2.7f) {
|
|
335
|
+
val now = System.currentTimeMillis()
|
|
336
|
+
if (now - lastShakeTime > 1000) {
|
|
337
|
+
lastShakeTime = now
|
|
338
|
+
emitDeviceShake()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
override fun onAccuracyChanged(sensor: android.hardware.Sensor?, accuracy: Int) {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
init {
|
|
346
|
+
try {
|
|
347
|
+
sensorManager = reactContext.getSystemService(Context.SENSOR_SERVICE) as? android.hardware.SensorManager
|
|
348
|
+
val accelerometer = sensorManager?.getDefaultSensor(android.hardware.Sensor.TYPE_ACCELEROMETER)
|
|
349
|
+
if (accelerometer != null) {
|
|
350
|
+
sensorManager?.registerListener(shakeListener, accelerometer, android.hardware.SensorManager.SENSOR_DELAY_UI)
|
|
351
|
+
}
|
|
352
|
+
} catch (e: Exception) {
|
|
353
|
+
Log.w(TAG, "Failed to register shake sensor listener", e)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fun emitDeviceShake() {
|
|
358
|
+
if (reactContext.hasActiveReactInstance()) {
|
|
359
|
+
reactContext
|
|
360
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
361
|
+
.emit("onDeviceShake", null)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@ReactMethod
|
|
366
|
+
fun startFpsMonitoring(promise: Promise) {
|
|
367
|
+
val activity = currentActivity ?: run {
|
|
368
|
+
promise.resolve(false)
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
activity.runOnUiThread {
|
|
373
|
+
if (!isFpsMonitoring) {
|
|
374
|
+
isFpsMonitoring = true
|
|
375
|
+
lastFrameTimeNanos = 0L
|
|
376
|
+
frameCount = 0
|
|
377
|
+
currentCalculatedFps = 60.0
|
|
378
|
+
android.view.Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
379
|
+
}
|
|
380
|
+
promise.resolve(true)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@ReactMethod
|
|
385
|
+
fun stopFpsMonitoring(promise: Promise) {
|
|
386
|
+
val activity = currentActivity ?: run {
|
|
387
|
+
promise.resolve(false)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
activity.runOnUiThread {
|
|
392
|
+
isFpsMonitoring = false
|
|
393
|
+
android.view.Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
394
|
+
promise.resolve(true)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@ReactMethod
|
|
399
|
+
fun getFpsMetrics(promise: Promise) {
|
|
400
|
+
val map = Arguments.createMap()
|
|
401
|
+
map.putDouble("fps", if (currentCalculatedFps > 0) currentCalculatedFps else 60.0)
|
|
402
|
+
map.putDouble("targetFps", 60.0)
|
|
403
|
+
promise.resolve(map)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@ReactMethod
|
|
407
|
+
fun getNativeStorageItem(key: String, promise: Promise) {
|
|
408
|
+
try {
|
|
409
|
+
val prefs = reactContext.getSharedPreferences("inapp_inspector_prefs", Context.MODE_PRIVATE)
|
|
410
|
+
val value = prefs.getString("inapp_inspector_$key", null)
|
|
411
|
+
promise.resolve(value)
|
|
412
|
+
} catch (e: Exception) {
|
|
413
|
+
promise.reject("STORAGE_ERROR", e.message, e)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@ReactMethod
|
|
418
|
+
fun setNativeStorageItem(key: String, value: String?, promise: Promise) {
|
|
419
|
+
try {
|
|
420
|
+
val prefs = reactContext.getSharedPreferences("inapp_inspector_prefs", Context.MODE_PRIVATE)
|
|
421
|
+
if (value == null) {
|
|
422
|
+
prefs.edit().remove("inapp_inspector_$key").apply()
|
|
423
|
+
} else {
|
|
424
|
+
prefs.edit().putString("inapp_inspector_$key", value).apply()
|
|
425
|
+
}
|
|
426
|
+
promise.resolve(true)
|
|
427
|
+
} catch (e: Exception) {
|
|
428
|
+
promise.reject("STORAGE_ERROR", e.message, e)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@ReactMethod
|
|
433
|
+
fun triggerHaptic(style: String, promise: Promise) {
|
|
434
|
+
try {
|
|
435
|
+
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
436
|
+
val vibratorManager = reactContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? android.os.VibratorManager
|
|
437
|
+
vibratorManager?.defaultVibrator
|
|
438
|
+
} else {
|
|
439
|
+
@Suppress("DEPRECATION")
|
|
440
|
+
reactContext.getSystemService(Context.VIBRATOR_SERVICE) as? android.os.Vibrator
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (vibrator != null && vibrator.hasVibrator()) {
|
|
444
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
445
|
+
val effect = when (style.lowercase()) {
|
|
446
|
+
"medium" -> android.os.VibrationEffect.createOneShot(20, android.os.VibrationEffect.DEFAULT_AMPLITUDE)
|
|
447
|
+
"heavy" -> android.os.VibrationEffect.createOneShot(35, android.os.VibrationEffect.DEFAULT_AMPLITUDE)
|
|
448
|
+
"success" -> {
|
|
449
|
+
val timings = longArrayOf(0, 15, 60, 20)
|
|
450
|
+
android.os.VibrationEffect.createWaveform(timings, -1)
|
|
451
|
+
}
|
|
452
|
+
"warning" -> {
|
|
453
|
+
val timings = longArrayOf(0, 25, 40, 25)
|
|
454
|
+
android.os.VibrationEffect.createWaveform(timings, -1)
|
|
455
|
+
}
|
|
456
|
+
"error" -> {
|
|
457
|
+
val timings = longArrayOf(0, 30, 40, 30, 40, 30)
|
|
458
|
+
android.os.VibrationEffect.createWaveform(timings, -1)
|
|
459
|
+
}
|
|
460
|
+
else -> android.os.VibrationEffect.createOneShot(10, 120)
|
|
461
|
+
}
|
|
462
|
+
vibrator.vibrate(effect)
|
|
463
|
+
} else {
|
|
464
|
+
@Suppress("DEPRECATION")
|
|
465
|
+
vibrator.vibrate(15)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
promise.resolve(true)
|
|
469
|
+
} catch (e: Exception) {
|
|
470
|
+
promise.resolve(false)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@ReactMethod
|
|
475
|
+
fun getNativeSystemMetrics(promise: Promise) {
|
|
476
|
+
metricsExecutor.execute {
|
|
477
|
+
try {
|
|
478
|
+
val map = Arguments.createMap()
|
|
479
|
+
|
|
480
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
481
|
+
val powerManager = reactContext.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
|
|
482
|
+
val status = powerManager?.currentThermalStatus ?: 0
|
|
483
|
+
val statusStr = when (status) {
|
|
484
|
+
android.os.PowerManager.THERMAL_STATUS_NONE -> "nominal"
|
|
485
|
+
android.os.PowerManager.THERMAL_STATUS_LIGHT -> "fair"
|
|
486
|
+
android.os.PowerManager.THERMAL_STATUS_MODERATE -> "fair"
|
|
487
|
+
android.os.PowerManager.THERMAL_STATUS_SEVERE -> "serious"
|
|
488
|
+
android.os.PowerManager.THERMAL_STATUS_CRITICAL -> "critical"
|
|
489
|
+
android.os.PowerManager.THERMAL_STATUS_EMERGENCY -> "critical"
|
|
490
|
+
android.os.PowerManager.THERMAL_STATUS_SHUTDOWN -> "critical"
|
|
491
|
+
else -> "nominal"
|
|
492
|
+
}
|
|
493
|
+
map.putString("thermalState", statusStr)
|
|
494
|
+
} else {
|
|
495
|
+
map.putString("thermalState", "nominal")
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
val memInfo = Debug.MemoryInfo()
|
|
499
|
+
Debug.getMemoryInfo(memInfo)
|
|
500
|
+
val residentRamMb = (memInfo.totalPss / 1024.0)
|
|
501
|
+
map.putDouble("residentRamMb", residentRamMb)
|
|
502
|
+
|
|
503
|
+
val actManager = reactContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
|
|
504
|
+
val actMemInfo = ActivityManager.MemoryInfo()
|
|
505
|
+
actManager?.getMemoryInfo(actMemInfo)
|
|
506
|
+
map.putDouble("totalPhysicalRamMb", (actMemInfo.totalMem / (1024.0 * 1024.0)))
|
|
507
|
+
|
|
508
|
+
map.putDouble("fps", if (currentCalculatedFps > 0) currentCalculatedFps else 60.0)
|
|
509
|
+
map.putInt("activeCpuCores", Runtime.getRuntime().availableProcessors())
|
|
510
|
+
|
|
511
|
+
promise.resolve(map)
|
|
512
|
+
} catch (e: Exception) {
|
|
513
|
+
promise.reject("METRICS_ERROR", e.message, e)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private val nativeNetworkLogs = java.util.Collections.synchronizedList(mutableListOf<String>())
|
|
519
|
+
private val nativeConsoleLogs = java.util.Collections.synchronizedList(mutableListOf<String>())
|
|
520
|
+
private val nativeAnalyticsEvents = java.util.Collections.synchronizedList(mutableListOf<String>())
|
|
521
|
+
private val nativeCrashRecords = java.util.Collections.synchronizedList(mutableListOf<String>())
|
|
522
|
+
|
|
523
|
+
@ReactMethod
|
|
524
|
+
fun pushNativeLogRecord(pageKey: String, jsonPayload: String, promise: Promise) {
|
|
525
|
+
if (jsonPayload.isBlank()) {
|
|
526
|
+
promise.resolve(false)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
val (executor, store) = when (pageKey) {
|
|
530
|
+
"logs" -> consoleExecutor to nativeConsoleLogs
|
|
531
|
+
"analytics" -> analyticsExecutor to nativeAnalyticsEvents
|
|
532
|
+
"crash" -> crashExecutor to nativeCrashRecords
|
|
533
|
+
else -> networkExecutor to nativeNetworkLogs
|
|
534
|
+
}
|
|
535
|
+
executor.execute {
|
|
536
|
+
try {
|
|
537
|
+
var updated = false
|
|
538
|
+
val idStr = try {
|
|
539
|
+
val jsonObj = org.json.JSONObject(jsonPayload)
|
|
540
|
+
if (jsonObj.has("id")) jsonObj.get("id").toString() else null
|
|
541
|
+
} catch (e: Exception) {
|
|
542
|
+
null
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
synchronized(store) {
|
|
546
|
+
if (idStr != null) {
|
|
547
|
+
val idPattern = "\"id\":$idStr"
|
|
548
|
+
val idStrPattern = "\"id\":\"$idStr\""
|
|
549
|
+
for (i in 0 until store.size) {
|
|
550
|
+
val existing = store[i]
|
|
551
|
+
if (existing.contains(idPattern) || existing.contains(idStrPattern)) {
|
|
552
|
+
store[i] = jsonPayload
|
|
553
|
+
updated = true
|
|
554
|
+
break
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!updated) {
|
|
560
|
+
store.add(0, jsonPayload)
|
|
561
|
+
if (store.size > 2000) {
|
|
562
|
+
store.removeAt(store.size - 1)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
promise.resolve(true)
|
|
567
|
+
} catch (e: Exception) {
|
|
568
|
+
promise.resolve(false)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
@ReactMethod
|
|
574
|
+
fun getNativeCachedPage(pageKey: String, offset: Int, limit: Int, query: String?, promise: Promise) {
|
|
575
|
+
val (executor, store) = when (pageKey) {
|
|
576
|
+
"logs" -> consoleExecutor to nativeConsoleLogs
|
|
577
|
+
"analytics" -> analyticsExecutor to nativeAnalyticsEvents
|
|
578
|
+
"crash" -> crashExecutor to nativeCrashRecords
|
|
579
|
+
else -> networkExecutor to nativeNetworkLogs
|
|
580
|
+
}
|
|
581
|
+
executor.execute {
|
|
582
|
+
try {
|
|
583
|
+
val cleanQuery = query?.trim()?.lowercase() ?: ""
|
|
584
|
+
val startIndex = Math.max(0, offset)
|
|
585
|
+
val actualLimit = if (limit > 0) limit else 50
|
|
586
|
+
val results = mutableListOf<String>()
|
|
587
|
+
|
|
588
|
+
synchronized(store) {
|
|
589
|
+
var itemsCollected = 0
|
|
590
|
+
for (i in startIndex until store.size) {
|
|
591
|
+
if (itemsCollected >= actualLimit) break
|
|
592
|
+
val itemJson = store[i]
|
|
593
|
+
if (cleanQuery.isNotEmpty()) {
|
|
594
|
+
if (itemJson.lowercase().contains(cleanQuery)) {
|
|
595
|
+
results.add(itemJson)
|
|
596
|
+
itemsCollected++
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
results.add(itemJson)
|
|
600
|
+
itemsCollected++
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
val jsonResult = org.json.JSONObject().apply {
|
|
605
|
+
put("pageKey", pageKey)
|
|
606
|
+
put("total", store.size)
|
|
607
|
+
put("offset", offset)
|
|
608
|
+
val jsonArr = org.json.JSONArray()
|
|
609
|
+
results.forEach { jsonArr.put(it) }
|
|
610
|
+
put("items", jsonArr)
|
|
611
|
+
}.toString()
|
|
612
|
+
|
|
613
|
+
promise.resolve(jsonResult)
|
|
614
|
+
}
|
|
615
|
+
} catch (e: Exception) {
|
|
616
|
+
promise.resolve("{\"items\":[],\"total\":0}")
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@ReactMethod
|
|
622
|
+
fun addListener(eventName: String) {
|
|
623
|
+
// Required for React Native NativeEventEmitter
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
@ReactMethod
|
|
627
|
+
fun removeListeners(count: Double) {
|
|
628
|
+
// Required for React Native NativeEventEmitter
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 100% Native Main-Thread Draggable Floating Button
|
|
634
|
+
* All touch processing and layout coordinates run directly in Android's UI Event Loop,
|
|
635
|
+
* completely decoupled and immune from JavaScript thread stalls.
|
|
636
|
+
*/
|
|
637
|
+
class InAppInspectorFloatingView(
|
|
638
|
+
context: Context,
|
|
639
|
+
private val onTap: () -> Unit
|
|
640
|
+
) : android.widget.FrameLayout(context) {
|
|
641
|
+
|
|
642
|
+
private var initialX = 0f
|
|
643
|
+
private var initialY = 0f
|
|
644
|
+
private var touchDownX = 0f
|
|
645
|
+
private var touchDownY = 0f
|
|
646
|
+
private var touchDownTime = 0L
|
|
647
|
+
private var isDragging = false
|
|
648
|
+
private val badgeView: android.view.View
|
|
649
|
+
|
|
650
|
+
init {
|
|
651
|
+
val density = context.resources.displayMetrics.density
|
|
652
|
+
|
|
653
|
+
// 1. Circular Background with Glow
|
|
654
|
+
val bgDrawable = android.graphics.drawable.GradientDrawable().apply {
|
|
655
|
+
shape = android.graphics.drawable.GradientDrawable.OVAL
|
|
656
|
+
setColor(android.graphics.Color.parseColor("#0F172A"))
|
|
657
|
+
setStroke((2.2f * density).toInt(), android.graphics.Color.parseColor("#38BDF8"))
|
|
658
|
+
}
|
|
659
|
+
background = bgDrawable
|
|
660
|
+
elevation = 16f * density
|
|
661
|
+
|
|
662
|
+
// 2. Center Icon (Inspector Owl Vector)
|
|
663
|
+
val iconView = object : android.view.View(context) {
|
|
664
|
+
private val bodyPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
665
|
+
color = android.graphics.Color.parseColor("#202E55")
|
|
666
|
+
style = android.graphics.Paint.Style.FILL
|
|
667
|
+
}
|
|
668
|
+
private val beamPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
669
|
+
color = android.graphics.Color.parseColor("#38BDF8")
|
|
670
|
+
style = android.graphics.Paint.Style.STROKE
|
|
671
|
+
strokeWidth = 4.2f
|
|
672
|
+
strokeJoin = android.graphics.Paint.Join.ROUND
|
|
673
|
+
strokeCap = android.graphics.Paint.Cap.ROUND
|
|
674
|
+
}
|
|
675
|
+
private val wingPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
676
|
+
color = android.graphics.Color.parseColor("#1A2545")
|
|
677
|
+
style = android.graphics.Paint.Style.FILL
|
|
678
|
+
}
|
|
679
|
+
private val bellyPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
680
|
+
color = android.graphics.Color.parseColor("#33477A")
|
|
681
|
+
style = android.graphics.Paint.Style.FILL
|
|
682
|
+
}
|
|
683
|
+
private val screenBgPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
684
|
+
color = android.graphics.Color.parseColor("#0C1426")
|
|
685
|
+
style = android.graphics.Paint.Style.FILL
|
|
686
|
+
}
|
|
687
|
+
private val codeLinePaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
688
|
+
color = android.graphics.Color.parseColor("#8FD0EC")
|
|
689
|
+
style = android.graphics.Paint.Style.STROKE
|
|
690
|
+
strokeWidth = 3f
|
|
691
|
+
strokeCap = android.graphics.Paint.Cap.ROUND
|
|
692
|
+
strokeJoin = android.graphics.Paint.Join.ROUND
|
|
693
|
+
}
|
|
694
|
+
private val eyeIrisPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
695
|
+
color = android.graphics.Color.parseColor("#FBBF24")
|
|
696
|
+
style = android.graphics.Paint.Style.FILL
|
|
697
|
+
}
|
|
698
|
+
private val eyePupilPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
699
|
+
color = android.graphics.Color.parseColor("#0A0E18")
|
|
700
|
+
style = android.graphics.Paint.Style.FILL
|
|
701
|
+
}
|
|
702
|
+
private val eyeGlintPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
703
|
+
color = android.graphics.Color.WHITE
|
|
704
|
+
style = android.graphics.Paint.Style.FILL
|
|
705
|
+
}
|
|
706
|
+
private val beakPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
707
|
+
color = android.graphics.Color.parseColor("#FB923C")
|
|
708
|
+
style = android.graphics.Paint.Style.FILL
|
|
709
|
+
}
|
|
710
|
+
private val lensHandlePaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
711
|
+
color = android.graphics.Color.parseColor("#38BDF8")
|
|
712
|
+
style = android.graphics.Paint.Style.STROKE
|
|
713
|
+
strokeWidth = 8.5f
|
|
714
|
+
strokeCap = android.graphics.Paint.Cap.ROUND
|
|
715
|
+
}
|
|
716
|
+
private val lensRingPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
717
|
+
color = android.graphics.Color.parseColor("#38BDF8")
|
|
718
|
+
style = android.graphics.Paint.Style.STROKE
|
|
719
|
+
strokeWidth = 7f
|
|
720
|
+
}
|
|
721
|
+
private val blushPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
|
|
722
|
+
color = android.graphics.Color.parseColor("#80FB7185")
|
|
723
|
+
style = android.graphics.Paint.Style.FILL
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private val path = android.graphics.Path()
|
|
727
|
+
|
|
728
|
+
override fun onDraw(canvas: android.graphics.Canvas) {
|
|
729
|
+
super.onDraw(canvas)
|
|
730
|
+
val w = width.toFloat()
|
|
731
|
+
val scale = (w * 0.94f) / 256f
|
|
732
|
+
val offset = (w - (w * 0.94f)) / 2f
|
|
733
|
+
|
|
734
|
+
canvas.save()
|
|
735
|
+
canvas.translate(offset, offset)
|
|
736
|
+
canvas.scale(scale, scale)
|
|
737
|
+
|
|
738
|
+
// 1. Owl Body with Ears
|
|
739
|
+
path.reset()
|
|
740
|
+
path.moveTo(62f, 150f)
|
|
741
|
+
path.cubicTo(58f, 104f, 70f, 70f, 90f, 58f)
|
|
742
|
+
path.lineTo(98f, 42f)
|
|
743
|
+
path.lineTo(116f, 62f)
|
|
744
|
+
path.quadTo(128f, 57f, 140f, 62f)
|
|
745
|
+
path.lineTo(158f, 42f)
|
|
746
|
+
path.lineTo(166f, 58f)
|
|
747
|
+
path.cubicTo(186f, 70f, 198f, 104f, 194f, 150f)
|
|
748
|
+
path.cubicTo(198f, 180f, 184f, 204f, 152f, 212f)
|
|
749
|
+
path.cubicTo(140f, 216f, 116f, 216f, 104f, 212f)
|
|
750
|
+
path.cubicTo(72f, 204f, 58f, 180f, 62f, 150f)
|
|
751
|
+
path.close()
|
|
752
|
+
canvas.drawPath(path, bodyPaint)
|
|
753
|
+
canvas.drawPath(path, beamPaint)
|
|
754
|
+
|
|
755
|
+
// 2. Wings
|
|
756
|
+
path.reset()
|
|
757
|
+
path.moveTo(74f, 124f)
|
|
758
|
+
path.cubicTo(58f, 154f, 60f, 190f, 86f, 204f)
|
|
759
|
+
path.cubicTo(79f, 176f, 77f, 148f, 88f, 126f)
|
|
760
|
+
path.close()
|
|
761
|
+
canvas.drawPath(path, wingPaint)
|
|
762
|
+
|
|
763
|
+
path.reset()
|
|
764
|
+
path.moveTo(182f, 124f)
|
|
765
|
+
path.cubicTo(198f, 154f, 196f, 190f, 170f, 204f)
|
|
766
|
+
path.cubicTo(177f, 176f, 179f, 148f, 168f, 126f)
|
|
767
|
+
path.close()
|
|
768
|
+
canvas.drawPath(path, wingPaint)
|
|
769
|
+
|
|
770
|
+
// 3. Belly Plate
|
|
771
|
+
path.reset()
|
|
772
|
+
path.moveTo(128f, 126f)
|
|
773
|
+
path.cubicTo(151f, 126f, 164f, 148f, 162f, 174f)
|
|
774
|
+
path.cubicTo(160f, 198f, 146f, 212f, 128f, 212f)
|
|
775
|
+
path.cubicTo(110f, 212f, 96f, 198f, 94f, 174f)
|
|
776
|
+
path.cubicTo(92f, 148f, 105f, 126f, 128f, 126f)
|
|
777
|
+
path.close()
|
|
778
|
+
canvas.drawPath(path, bellyPaint)
|
|
779
|
+
|
|
780
|
+
// 4. Chest Screen `< / >`
|
|
781
|
+
val rectF = android.graphics.RectF(107f, 161f, 149f, 193f)
|
|
782
|
+
canvas.drawRoundRect(rectF, 8f, 8f, screenBgPaint)
|
|
783
|
+
val screenStroke = android.graphics.Paint(beamPaint).apply { strokeWidth = 2f }
|
|
784
|
+
canvas.drawRoundRect(rectF, 8f, 8f, screenStroke)
|
|
785
|
+
|
|
786
|
+
path.reset()
|
|
787
|
+
path.moveTo(122f, 170f); path.lineTo(115f, 177f); path.lineTo(122f, 184f)
|
|
788
|
+
path.moveTo(134f, 170f); path.lineTo(141f, 177f); path.lineTo(134f, 184f)
|
|
789
|
+
path.moveTo(130f, 168f); path.lineTo(126f, 186f)
|
|
790
|
+
canvas.drawPath(path, codeLinePaint)
|
|
791
|
+
|
|
792
|
+
// 5. Normal Right Eye
|
|
793
|
+
canvas.drawCircle(153f, 107f, 17f, eyeIrisPaint)
|
|
794
|
+
canvas.drawCircle(153f, 108f, 8.2f, eyePupilPaint)
|
|
795
|
+
canvas.drawCircle(156.2f, 104f, 3.1f, eyeGlintPaint)
|
|
796
|
+
|
|
797
|
+
// 6. Beak
|
|
798
|
+
path.reset()
|
|
799
|
+
path.moveTo(123.5f, 123f)
|
|
800
|
+
path.quadTo(128f, 121f, 132.5f, 123f)
|
|
801
|
+
path.quadTo(131f, 132f, 128f, 134.5f)
|
|
802
|
+
path.quadTo(125f, 132f, 123.5f, 123f)
|
|
803
|
+
path.close()
|
|
804
|
+
canvas.drawPath(path, beakPaint)
|
|
805
|
+
|
|
806
|
+
// 7. Magnifier Debug Left Eye
|
|
807
|
+
canvas.drawCircle(95f, 103f, 20f, eyeIrisPaint)
|
|
808
|
+
canvas.drawCircle(95f, 104f, 10f, eyePupilPaint)
|
|
809
|
+
canvas.drawCircle(98.6f, 100f, 3.4f, eyeGlintPaint)
|
|
810
|
+
|
|
811
|
+
// Magnifier Lens Ring & Handle
|
|
812
|
+
canvas.drawLine(75f, 123f, 54f, 147f, lensHandlePaint)
|
|
813
|
+
canvas.drawCircle(95f, 103f, 28f, lensRingPaint)
|
|
814
|
+
|
|
815
|
+
// Rosy Cheeks
|
|
816
|
+
canvas.drawOval(android.graphics.RectF(74f, 121f, 92f, 133f), blushPaint)
|
|
817
|
+
canvas.drawOval(android.graphics.RectF(158f, 116f, 176f, 128f), blushPaint)
|
|
818
|
+
|
|
819
|
+
canvas.restore()
|
|
820
|
+
}
|
|
821
|
+
}.apply {
|
|
822
|
+
isClickable = false
|
|
823
|
+
isFocusable = false
|
|
824
|
+
}
|
|
825
|
+
addView(iconView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
826
|
+
|
|
827
|
+
// 3. Status Badge Dot (Top-Right)
|
|
828
|
+
val badgeSize = (10f * density).toInt()
|
|
829
|
+
badgeView = android.view.View(context).apply {
|
|
830
|
+
val badgeDrawable = android.graphics.drawable.GradientDrawable().apply {
|
|
831
|
+
shape = android.graphics.drawable.GradientDrawable.OVAL
|
|
832
|
+
setColor(android.graphics.Color.parseColor("#22C55E"))
|
|
833
|
+
setStroke((1.5f * density).toInt(), android.graphics.Color.parseColor("#0F172A"))
|
|
834
|
+
}
|
|
835
|
+
background = badgeDrawable
|
|
836
|
+
visibility = GONE
|
|
837
|
+
}
|
|
838
|
+
val badgeParams = LayoutParams(badgeSize, badgeSize).apply {
|
|
839
|
+
gravity = android.view.Gravity.TOP or android.view.Gravity.END
|
|
840
|
+
topMargin = (4f * density).toInt()
|
|
841
|
+
rightMargin = (4f * density).toInt()
|
|
842
|
+
}
|
|
843
|
+
addView(badgeView, badgeParams)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fun setBadgeVisible(visible: Boolean) {
|
|
847
|
+
badgeView.visibility = if (visible) VISIBLE else GONE
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
|
|
851
|
+
val parentView = parent as? android.view.ViewGroup ?: return super.onTouchEvent(event)
|
|
852
|
+
val density = context.resources.displayMetrics.density
|
|
853
|
+
|
|
854
|
+
when (event.actionMasked) {
|
|
855
|
+
android.view.MotionEvent.ACTION_DOWN -> {
|
|
856
|
+
parentView.bringChildToFront(this)
|
|
857
|
+
initialX = x
|
|
858
|
+
initialY = y
|
|
859
|
+
touchDownX = event.rawX
|
|
860
|
+
touchDownY = event.rawY
|
|
861
|
+
touchDownTime = System.currentTimeMillis()
|
|
862
|
+
isDragging = false
|
|
863
|
+
animate().scaleX(0.92f).scaleY(0.92f).setDuration(100).start()
|
|
864
|
+
return true
|
|
865
|
+
}
|
|
866
|
+
android.view.MotionEvent.ACTION_MOVE -> {
|
|
867
|
+
val dx = event.rawX - touchDownX
|
|
868
|
+
val dy = event.rawY - touchDownY
|
|
869
|
+
|
|
870
|
+
if (!isDragging && Math.hypot(dx.toDouble(), dy.toDouble()) > 8 * density) {
|
|
871
|
+
isDragging = true
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (isDragging) {
|
|
875
|
+
val minX = 10f * density
|
|
876
|
+
val maxX = (parentView.width - width - 10f * density).coerceAtLeast(minX)
|
|
877
|
+
val minY = 36f * density
|
|
878
|
+
val maxY = (parentView.height - height - 36f * density).coerceAtLeast(minY)
|
|
879
|
+
|
|
880
|
+
x = (initialX + dx).coerceIn(minX, maxX)
|
|
881
|
+
y = (initialY + dy).coerceIn(minY, maxY)
|
|
882
|
+
}
|
|
883
|
+
return true
|
|
884
|
+
}
|
|
885
|
+
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
886
|
+
animate().scaleX(1.0f).scaleY(1.0f).setDuration(120).start()
|
|
887
|
+
val elapsed = System.currentTimeMillis() - touchDownTime
|
|
888
|
+
val totalDist = Math.hypot((event.rawX - touchDownX).toDouble(), (event.rawY - touchDownY).toDouble())
|
|
889
|
+
|
|
890
|
+
if (!isDragging && totalDist < 12 * density && elapsed < 350) {
|
|
891
|
+
onTap()
|
|
892
|
+
} else if (isDragging) {
|
|
893
|
+
// Smooth native snap to nearest horizontal side
|
|
894
|
+
val minX = 10f * density
|
|
895
|
+
val maxX = (parentView.width - width - 10f * density).coerceAtLeast(minX)
|
|
896
|
+
val targetX = if (x < parentView.width / 2f) minX else maxX
|
|
897
|
+
animate().x(targetX).setDuration(220).setInterpolator(android.view.animation.DecelerateInterpolator()).start()
|
|
898
|
+
}
|
|
899
|
+
return true
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return super.onTouchEvent(event)
|
|
903
|
+
}
|
|
904
|
+
}
|