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 { val dir = getCrashLogDir() if (!dir.exists()) return emptyList() return dir.listFiles { f -> f.name.startsWith("crash_") && f.name.endsWith(".log") } ?.sortedByDescending { it.lastModified() } ?: emptyList() } }