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

213 lines
8.6 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.stand.standapp.utils
import android.content.Context
import com.stand.standapp.AppConfig
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 = OkHttpClient()
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)
}
// 2. 捕获主线程 Looper
android.os.Handler(android.os.Looper.getMainLooper()).post {
while (true) {
try {
android.os.Looper.loop()
} catch (e: Throwable) {
onCrashDetected(null, "MainLooper", e)
// 如果是 UnsatisfiedLinkError这种错误无法恢复必须抛出让系统处理
throw e
}
}
}
}
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?) {
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) {}
}
}
}
}