203 lines
8.2 KiB
Kotlin
203 lines
8.2 KiB
Kotlin
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 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) {}
|
||
}
|
||
}
|
||
}
|
||
}
|