package com.stand.standapp.utils import android.content.Context import com.stand.standapp.AppConfig import com.stand.standapp.net.NetworkModule import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.asRequestBody import timber.log.Timber import java.io.* import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.Executors import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.system.exitProcess object LogManager { private val executor = Executors.newSingleThreadExecutor() private const val MAX_DAYS = 15 private val client = NetworkModule.defaultClient private var logDir: File? = null fun init(context: Context) { Timber.plant(Timber.DebugTree()) try { // 1. 路径确定 val baseDir = context.getExternalFilesDir(null) logDir = if (baseDir != null) { File(baseDir, "logs") } else { File(context.filesDir, "logs") } if (logDir?.exists() == false) { val created = logDir?.mkdirs() android.util.Log.i("LogManager", "Directory created: $created at ${logDir?.absolutePath}") } // 2. 强行创建一个“自检”日志文件,不使用异步线程池 logDir?.let { dir -> val testFile = File(dir, "startup_check.txt") try { val fos = FileOutputStream(testFile, true) fos.write("Logging started at ${Date()}\n".toByteArray()) fos.close() android.util.Log.i("LogManager", "Startup check file written successfully") } catch (e: Exception) { android.util.Log.e("LogManager", "FAILED to write startup check file", e) } } logDir?.let { cleanOldLogs(it) Timber.plant(FileLoggingTree(it)) } setupCrashHandler() } catch (e: Exception) { android.util.Log.e("LogManager", "Init global failure", e) } } /** * 专门供外部(如 xCrash 或其它处理器)调用的保存方法 */ fun onCrashDetected(logPath: String?, threadName: String?, throwable: Throwable?) { val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date()) val crashInfo = StringBuilder().apply { append("\n\n=== [DETECTION] CRASH LOG ===\n") append("Time: $timestamp\n") append("Thread: ${threadName ?: "Unknown"}\n") logPath?.let { append("XCrash Report: $it\n") } throwable?.let { append("Exception: ${it.javaClass.simpleName}\n") append("Message: ${it.message}\n") append("Stacktrace:\n${it.stackTraceToString()}\n") } append("=== END OF DETECTION ===\n\n") }.toString() logDir?.let { dir -> try { val logFile = File(dir, SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + ".log") FileOutputStream(logFile, true).use { it.write(crashInfo.toByteArray()) it.flush() it.fd.sync() } } catch (e: Exception) {} } } private fun setupCrashHandler() { // 1. 捕获所有线程 val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> onCrashDetected(null, thread.name, throwable) defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1) } } private fun cleanOldLogs(dir: File) { executor.execute { try { val thresholdDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -MAX_DAYS) }.time dir.listFiles()?.forEach { file -> if (file.isFile && file.name.endsWith(".log") && Date(file.lastModified()).before(thresholdDate)) { file.delete() } } } catch (e: Exception) {} } } fun zipLogs(context: Context): String? { val zipFile = File(context.getExternalFilesDir(null), "full_logs_${System.currentTimeMillis()}.zip") val logFolder = logDir // 这通常是 files/logs val tombstoneFolder = File(context.getExternalFilesDir(null), "tombstones") return try { ZipOutputStream(FileOutputStream(zipFile)).use { zos -> // 1. 打包 logs 文件夹 logFolder?.let { addFolderToZip(zos, it, "logs") } // 2. 打包 tombstones 文件夹 if (tombstoneFolder.exists()) { addFolderToZip(zos, tombstoneFolder, "tombstones") } } zipFile.absolutePath } catch (e: Exception) { android.util.Log.e("LogManager", "Zip failed", e) null } } private fun addFolderToZip(zos: ZipOutputStream, folder: File, parentName: String) { folder.listFiles()?.forEach { file -> if (file.isFile) { try { val entry = ZipEntry("$parentName/${file.name}") zos.putNextEntry(entry) FileInputStream(file).use { it.copyTo(zos) } zos.closeEntry() } catch (e: Exception) {} } } } fun shutdown() { Timber.tag("LogManager").i("Shutting down...") try { executor.shutdown() executor.awaitTermination(3, java.util.concurrent.TimeUnit.SECONDS) } catch (e: Exception) { Timber.tag("LogManager").e(e, "Executor shutdown failed") } } fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) { executor.execute { val zipPath = zipLogs(context) ?: return@execute callback(false, "打包失败") val serverUrl = AppConfig.getServerUrl(context) val uploadUrl = "$serverUrl/api/attachment/upload" val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQ5MDczNSwianRpIjoiMjYzNjljYzQtYzJiMy00YTg0LWFiYTMtZDU4NmNiZDRmMmM0IiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.AyuzNAe-R_GOBC65STH5qpHkmmUngX8rMBPI4CdIIhk" val body = MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("file", File(zipPath).name, File(zipPath).asRequestBody("application/zip".toMediaTypeOrNull())) .build() val request = Request.Builder().url(uploadUrl).header("Authorization", "Bearer $bearerToken").post(body).build() try { client.newCall(request).execute().use { response -> if (response.isSuccessful) { callback(true, "上传成功") File(zipPath).delete() } else callback(false, "错误: ${response.code}") } } catch (e: IOException) { callback(false, "失败: ${e.message}") } } } private class FileLoggingTree(private val logDir: File) : Timber.Tree() { private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) private val fileNameFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { // 过滤掉高频的 VERBOSE 级别日志写入(例如录音帧),保留在控制台输出 if (priority == android.util.Log.VERBOSE) return executor.execute { try { val logFile = File(logDir, "${fileNameFormat.format(Date())}.log") val priorityStr = when (priority) { 2->"V" 3->"D" 4->"I" 5->"W" 6->"E" 7->"A" else->"U" } val entry = "${dateFormat.format(Date())} $priorityStr/[$tag]: $message\n${t?.stackTraceToString() ?: ""}\n" FileWriter(logFile, true).use { it.write(entry) } } catch (e: Exception) {} } } } }