添加了崩溃抓取
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.sw.dualscreen
|
package com.sw.dualscreen
|
||||||
|
|
||||||
|
import com.sw.dualscreen.utils.CrashHandler
|
||||||
import com.sw.plate.App
|
import com.sw.plate.App
|
||||||
import com.sw.plate.utils.AppUtil
|
import com.sw.plate.utils.AppUtil
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -16,5 +17,8 @@ class MyApp : App() {
|
|||||||
Timber.d("UDID = ${AppUtil.getUDID(this)}")
|
Timber.d("UDID = ${AppUtil.getUDID(this)}")
|
||||||
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
||||||
GlobalData.deviceId = deviceId
|
GlobalData.deviceId = deviceId
|
||||||
|
|
||||||
|
// 初始化崩溃处理器
|
||||||
|
CrashHandler.init(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,9 +114,9 @@ object SensorScaleUtils {
|
|||||||
* 零位标定
|
* 零位标定
|
||||||
*/
|
*/
|
||||||
fun zero() {
|
fun zero() {
|
||||||
Timber.d("zero isZero = $isZero")
|
Timber.d("zero isOpened = $isOpened")
|
||||||
if (isZero) return
|
if (!isOpened) return
|
||||||
isZero = true
|
// isZero = true
|
||||||
mSensorScale?.zero {
|
mSensorScale?.zero {
|
||||||
Timber.d("零位标定操作成功")
|
Timber.d("零位标定操作成功")
|
||||||
ToastUtils.showToast("零位标定操作成功")
|
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.databinding.ItemSearchFoodInfoBinding
|
||||||
import com.sw.dualscreen.model.response.FoodInfo
|
import com.sw.dualscreen.model.response.FoodInfo
|
||||||
import com.sw.dualscreen.utils.Debouncer
|
import com.sw.dualscreen.utils.Debouncer
|
||||||
|
import com.sw.dualscreen.utils.KeyboardUtils
|
||||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -95,13 +96,8 @@ class CustomBottomSheetDialog(
|
|||||||
binding.ivSearch.setOnClickListener {
|
binding.ivSearch.setOnClickListener {
|
||||||
searchInfo()
|
searchInfo()
|
||||||
}
|
}
|
||||||
binding.etSearch.setOnEditorActionListener { _, actionId, _ ->
|
KeyboardUtils.setupEditorAction(binding.etSearch) {
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
|
||||||
searchInfo()
|
searchInfo()
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
registerDataChange()
|
registerDataChange()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="@android:color/white" />
|
<solid android:color="@android:color/white" />
|
||||||
<corners
|
<corners
|
||||||
android:topLeftRadius="16dp"
|
android:topLeftRadius="60dp"
|
||||||
android:topRightRadius="16dp" />
|
android:topRightRadius="60dp" />
|
||||||
</shape>
|
</shape>
|
||||||
Reference in New Issue
Block a user