添加了崩溃抓取

This commit is contained in:
zxj
2025-08-07 17:10:59 +08:00
parent c3489ab9e6
commit 2e20e7deab
6 changed files with 317 additions and 12 deletions
@@ -1,5 +1,6 @@
package com.sw.dualscreen
import com.sw.dualscreen.utils.CrashHandler
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import timber.log.Timber
@@ -16,5 +17,8 @@ class MyApp : App() {
Timber.d("UDID = ${AppUtil.getUDID(this)}")
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId
// 初始化崩溃处理器
CrashHandler.init(this)
}
}
@@ -114,9 +114,9 @@ object SensorScaleUtils {
* 零位标定
*/
fun zero() {
Timber.d("zero isZero = $isZero")
if (isZero) return
isZero = true
Timber.d("zero isOpened = $isOpened")
if (!isOpened) return
// isZero = true
mSensorScale?.zero {
Timber.d("零位标定操作成功")
ToastUtils.showToast("零位标定操作成功")
@@ -0,0 +1,246 @@
package com.sw.dualscreen.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.dualscreen.activity.MainActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.apply
import kotlin.collections.forEach
import kotlin.io.bufferedReader
import kotlin.io.readLines
import kotlin.io.use
import kotlin.jvm.java
import kotlin.run
import kotlin.system.exitProcess
import kotlin.text.endsWith
import kotlin.text.toByteArray
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Timber.tag(TAG).e("Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -0,0 +1,59 @@
package com.sw.dualscreen.utils
import android.app.Activity
import android.content.Context
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
/**
* 键盘工具类
*/
object KeyboardUtils {
/**
* 显示键盘
*/
fun showKeyboard(editText: EditText) {
editText.requestFocus()
val imm = editText.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
/**
* 隐藏键盘
*/
fun hideKeyboard(activity: Activity) {
val view = activity.currentFocus ?: activity.window.decorView
hideKeyboard(view)
}
/**
* 隐藏键盘
*/
fun hideKeyboard(view: View) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
/**
* 为EditText设置EditorAction监听并隐藏键盘
*/
fun setupEditorAction(editText: EditText, onAction: (() -> Unit)? = null) {
editText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
actionId == EditorInfo.IME_ACTION_SEARCH ||
actionId == EditorInfo.IME_ACTION_GO ||
actionId == EditorInfo.IME_ACTION_SEND ||
actionId == EditorInfo.IME_ACTION_NEXT) {
hideKeyboard(editText)
onAction?.invoke()
true
} else {
false
}
}
}
}
@@ -20,6 +20,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.KeyboardUtils
import com.sw.dualscreen.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -95,13 +96,8 @@ class CustomBottomSheetDialog(
binding.ivSearch.setOnClickListener {
searchInfo()
}
binding.etSearch.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchInfo()
true
} else {
false
}
KeyboardUtils.setupEditorAction(binding.etSearch) {
searchInfo()
}
registerDataChange()
}
@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="16dp" />
android:topLeftRadius="60dp"
android:topRightRadius="60dp" />
</shape>