diff --git a/sdk-bugcollect/src/main/AndroidManifest.xml b/sdk-bugcollect/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ff7a1f0
--- /dev/null
+++ b/sdk-bugcollect/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt
index 70cab38..1ee6442 100644
--- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt
+++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt
@@ -129,11 +129,14 @@ object BugCollect {
event("__log_info", tags + ("message" to message))
}
+ /**
+ * 注册全局崩溃拦截器(幂等)。
+ * 通常由 BugCollectInitProvider 在 app 启动时自动调用,无需 app 代码主动触发。
+ * 如需在特定时机手动注册,调用此方法也是安全的。
+ */
fun startCrashCapture() {
if (crashCaptureStarted) return
if (!XuqmSDK.isInitialized()) return
- // bugCollectApiUrl 由远端配置下发,启动时可能尚未到达。
- // 用 platformUrl 推导兜底地址:崩溃文件里会保存此 URL,下次启动时用它上报。
val logApiUrl = XuqmSDK.bugCollectApiUrl
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
crashCaptureStarted = true
@@ -146,6 +149,11 @@ object BugCollect {
queue().uploadPendingCrashes()
}
+ /** 上传上次崩溃落盘的文件(远端配置就绪后由 BugCollectInitProvider 自动调用)。 */
+ fun uploadPendingCrashes() {
+ if (isReady()) queue().uploadPendingCrashes()
+ }
+
fun defineFunnel(id: String, steps: List) {
FunnelTracker.define(id, steps)
}
diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt
new file mode 100644
index 0000000..d139c78
--- /dev/null
+++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt
@@ -0,0 +1,75 @@
+package com.xuqm.sdk.bugcollect.internal
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.util.Log
+import com.xuqm.sdk.XuqmSDK
+import com.xuqm.sdk.bugcollect.BugCollect
+import com.xuqm.sdk.bugcollect.CrashCapture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+/**
+ * 崩溃收集的早期初始化入口。
+ *
+ * initOrder=89,在 sdk-core XuqmInitializerProvider(90) 之后运行,此时:
+ * - XuqmSDK.appKey / platformUrl 已由本地 config 文件同步设置
+ * - 远端平台配置尚未到达(bugCollectApiUrl 可能还是 null)
+ *
+ * 职责:
+ * 1. 立即注册 UncaughtExceptionHandler,把崩溃落盘(只需 appKey + 派生 URL)
+ * 2. 后台等待 XuqmSDK.awaitInitialization() 完成后,上传积压的崩溃文件
+ *
+ * sdk-core 不依赖本类,XuqmSDK 可独立使用。
+ */
+internal class BugCollectInitProvider : ContentProvider() {
+
+ override fun onCreate(): Boolean {
+ val ctx = context?.applicationContext ?: return true
+
+ if (!XuqmSDK.isInitialized()) {
+ // core 未初始化(config 文件缺失等),跳过
+ Log.w("BugCollect", "BugCollectInitProvider: XuqmSDK not initialized, skipping crash capture setup")
+ return true
+ }
+
+ // ── Phase 1: 立即注册崩溃拦截,落盘不需要远端配置 ──────────────────────
+ val logApiUrl = XuqmSDK.bugCollectApiUrl
+ ?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
+
+ CrashCapture.start(
+ appContext = ctx,
+ logApiUrl = logApiUrl,
+ appKey = XuqmSDK.appKey,
+ getUserId = { XuqmSDK.getUserId() },
+ )
+
+ // ── Phase 2: 等远端配置就绪后上传积压的崩溃文件 ─────────────────────────
+ uploadAfterInit(ctx)
+
+ return true
+ }
+
+ private fun uploadAfterInit(ctx: Context) {
+ CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
+ runCatching {
+ XuqmSDK.awaitInitialization()
+ // 远端配置到达后 bugCollectApiUrl 已更新,用最新 URL 上传
+ BugCollect.uploadPendingCrashes()
+ }.onFailure { e ->
+ Log.w("BugCollect", "Failed to upload pending crashes after init: ${e.message}")
+ }
+ }
+ }
+
+ override fun query(uri: Uri, p: Array?, s: String?, sa: Array?, so: String?): Cursor? = null
+ override fun getType(uri: Uri): String? = null
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0
+}
diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt
index f17b8c9..819af2d 100644
--- a/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt
+++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt
@@ -19,26 +19,12 @@ class XuqmInitializerProvider : ContentProvider() {
if (!XuqmSDK.isInitialized()) {
XuqmSDK.autoInitialize(ctx)
}
- // 在 SDK 初始化后立即启动崩溃捕获,确保任何后续崩溃都能被落盘。
- // startCrashCapture 内部使用 platformUrl 兜底,无需等远端配置到达。
- startCrashCaptureIfAvailable()
}.onFailure { e ->
Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}")
}
return true
}
- private fun startCrashCaptureIfAvailable() {
- runCatching {
- val bugCollect = Class.forName("com.xuqm.sdk.bugcollect.BugCollect")
- val companion = bugCollect.getField("INSTANCE").get(null)
- val method = bugCollect.getMethod("startCrashCapture")
- method.invoke(companion)
- }.onFailure {
- // sdk-bugcollect 未依赖时跳过,不影响其他功能
- }
- }
-
override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null