This commit is contained in:
wdvipa
2026-02-11 16:59:49 +08:00
commit eee3a16150
3327 changed files with 198527 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
package com.hikoncont.crash
import android.content.Context
import android.os.Build
import android.util.Log
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 全局未捕获异常处理器
* 捕获崩溃信息写入本地文件,重启后由 CrashLogUploader 上传服务器
*/
class CrashHandler private constructor() : Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_DIR = "crash_logs"
private const val MAX_LOG_FILES = 20 // 最多保留20个崩溃日志
@Volatile
private var instance: CrashHandler? = null
fun getInstance(): CrashHandler {
return instance ?: synchronized(this) {
instance ?: CrashHandler().also { instance = it }
}
}
}
private var defaultHandler: Thread.UncaughtExceptionHandler? = null
private lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
Log.i(TAG, "✅ 崩溃处理器已初始化")
}
override fun uncaughtException(thread: Thread, throwable: Throwable) {
try {
saveCrashLog(thread, throwable)
} catch (e: Exception) {
Log.e(TAG, "❌ 保存崩溃日志失败", e)
}
// 交给系统默认处理器(终止进程)
defaultHandler?.uncaughtException(thread, throwable)
}
private fun saveCrashLog(thread: Thread, throwable: Throwable) {
val timestamp = System.currentTimeMillis()
val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault())
val fileName = "crash_${dateFormat.format(Date(timestamp))}_$timestamp.log"
val sb = StringBuilder()
// 设备信息
sb.appendLine("=== 崩溃报告 ===")
sb.appendLine("时间: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date(timestamp))}")
sb.appendLine("时间戳: $timestamp")
sb.appendLine("线程: ${thread.name} (id=${thread.id})")
sb.appendLine()
sb.appendLine("=== 设备信息 ===")
sb.appendLine("品牌: ${Build.BRAND}")
sb.appendLine("型号: ${Build.MODEL}")
sb.appendLine("制造商: ${Build.MANUFACTURER}")
sb.appendLine("Android版本: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
sb.appendLine("指纹: ${Build.FINGERPRINT}")
try {
val pm = appContext.packageManager
val pi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(appContext.packageName, android.content.pm.PackageManager.PackageInfoFlags.of(0))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(appContext.packageName, 0)
}
sb.appendLine("应用版本: ${pi.versionName} (${pi.longVersionCode})")
} catch (_: Exception) {
sb.appendLine("应用版本: unknown")
}
sb.appendLine("包名: ${appContext.packageName}")
sb.appendLine()
// 堆栈信息
sb.appendLine("=== 异常堆栈 ===")
val sw = StringWriter()
throwable.printStackTrace(PrintWriter(sw))
sb.append(sw.toString())
// 写入文件
val crashDir = File(appContext.filesDir, CRASH_DIR)
if (!crashDir.exists()) crashDir.mkdirs()
val crashFile = File(crashDir, fileName)
crashFile.writeText(sb.toString())
Log.e(TAG, "💾 崩溃日志已保存: ${crashFile.absolutePath}")
// 清理旧日志
cleanOldLogs(crashDir)
}
private fun cleanOldLogs(dir: File) {
val files = dir.listFiles { f -> f.name.startsWith("crash_") && f.name.endsWith(".log") }
?: return
if (files.size > MAX_LOG_FILES) {
files.sortBy { it.lastModified() }
for (i in 0 until files.size - MAX_LOG_FILES) {
files[i].delete()
Log.d(TAG, "🗑️ 清理旧崩溃日志: ${files[i].name}")
}
}
}
/**
* 获取崩溃日志目录
*/
fun getCrashLogDir(): File {
return File(appContext.filesDir, CRASH_DIR)
}
/**
* 获取所有待上传的崩溃日志文件
*/
fun getPendingCrashLogs(): List<File> {
val dir = getCrashLogDir()
if (!dir.exists()) return emptyList()
return dir.listFiles { f -> f.name.startsWith("crash_") && f.name.endsWith(".log") }
?.sortedByDescending { it.lastModified() }
?: emptyList()
}
}

View File

@@ -0,0 +1,102 @@
package com.hikoncont.crash
import android.content.Context
import android.os.Build
import android.util.Log
import io.socket.client.Socket
import kotlinx.coroutines.*
import org.json.JSONObject
/**
* 崩溃日志上传器
* 在应用重启后检查本地崩溃日志,通过 Socket.IO 上传到服务端
*/
class CrashLogUploader(private val context: Context) {
companion object {
private const val TAG = "CrashLogUploader"
private const val MAX_LOG_SIZE = 512 * 1024 // 单个日志最大512KB
private const val UPLOAD_DELAY_MS = 5000L // 启动后延迟5秒再上传等待Socket连接稳定
}
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* 在Socket连接成功后调用上传待处理的崩溃日志
*/
fun uploadPendingLogs(socket: Socket?, deviceId: String) {
scope.launch {
try {
delay(UPLOAD_DELAY_MS)
val crashHandler = CrashHandler.getInstance()
val pendingLogs = crashHandler.getPendingCrashLogs()
if (pendingLogs.isEmpty()) {
Log.d(TAG, "📋 无待上传的崩溃日志")
return@launch
}
Log.i(TAG, "📤 发现 ${pendingLogs.size} 个崩溃日志待上传")
for (logFile in pendingLogs) {
if (socket?.connected() != true) {
Log.w(TAG, "⚠️ Socket未连接停止上传")
break
}
try {
val content = logFile.readText()
if (content.length > MAX_LOG_SIZE) {
Log.w(TAG, "⚠️ 崩溃日志过大,截断: ${logFile.name} (${content.length}B)")
}
val payload = JSONObject().apply {
put("deviceId", deviceId)
put("fileName", logFile.name)
put("content", content.take(MAX_LOG_SIZE))
put("fileSize", logFile.length())
put("crashTime", extractCrashTimestamp(logFile.name))
put("uploadTime", System.currentTimeMillis())
put("deviceModel", Build.MODEL)
put("osVersion", "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
}
socket.emit("crash_log", payload)
Log.i(TAG, "✅ 已上传崩溃日志: ${logFile.name}")
// 上传成功后删除本地文件
logFile.delete()
Log.d(TAG, "🗑️ 已删除本地崩溃日志: ${logFile.name}")
// 间隔发送,避免拥塞
delay(500)
} catch (e: Exception) {
Log.e(TAG, "❌ 上传崩溃日志失败: ${logFile.name}", e)
}
}
Log.i(TAG, "📤 崩溃日志上传完成")
} catch (e: Exception) {
Log.e(TAG, "❌ 崩溃日志上传流程异常", e)
}
}
}
/**
* 从文件名提取崩溃时间戳
* 文件名格式: crash_2025-01-01_12-00-00_1234567890.log
*/
private fun extractCrashTimestamp(fileName: String): Long {
return try {
val parts = fileName.removeSuffix(".log").split("_")
parts.lastOrNull()?.toLongOrNull() ?: 0L
} catch (_: Exception) {
0L
}
}
fun destroy() {
scope.cancel()
}
}