StandAPP/app/src/main/java/com/stand/standapp/utils/LogManager.kt

204 lines
8.3 KiB
Kotlin

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) {}
}
}
}
}