135 lines
4.8 KiB
Kotlin
135 lines
4.8 KiB
Kotlin
|
|
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()
|
|||
|
|
}
|
|||
|
|
}
|