开发者
This commit is contained in:
parent
3302225e74
commit
76b6a7eae2
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0")
|
||||||
implementation("io.github.justson:agentweb-core:v5.1.1-androidx")
|
implementation("io.github.justson:agentweb-core:v5.1.1-androidx")
|
||||||
implementation("com.github.Justson:Downloader:v5.0.4-androidx")
|
implementation("com.github.Justson:Downloader:v5.0.4-androidx")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
|
|
||||||
|
|
@ -117,53 +117,93 @@ public class pttNative {
|
||||||
|
|
||||||
public void pttNativeSendAtCmd(String strAtCmd)
|
public void pttNativeSendAtCmd(String strAtCmd)
|
||||||
{
|
{
|
||||||
Log.i(TAG,"strAtCmd " + strAtCmd);
|
try {
|
||||||
OEM_AT_Send(strAtCmd);
|
Log.i(TAG,"strAtCmd " + strAtCmd);
|
||||||
|
OEM_AT_Send(strAtCmd);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.e(TAG, "Native AT Send failed: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pttNativePocTaskStart()
|
public void pttNativePocTaskStart()
|
||||||
{
|
{
|
||||||
OEM_Poc_Task_Start();
|
try {
|
||||||
|
OEM_Poc_Task_Start();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.e(TAG, "Native PocTaskStart failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetAtCallback()
|
public int pttNativeSetAtCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_AT_CallBack("OEM_AT_cb");
|
try {
|
||||||
|
return OEM_Set_AT_CallBack("OEM_AT_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetPlayCallback()
|
public int pttNativeSetPlayCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_Play_CallBack("OEM_Play_cb");
|
try {
|
||||||
|
return OEM_Set_Play_CallBack("OEM_Play_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetPalyStartCallback()
|
public int pttNativeSetPalyStartCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_Play_Start_Callback("OEM_Play_Start_cb");
|
try {
|
||||||
|
return OEM_Set_Play_Start_Callback("OEM_Play_Start_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetPlayStopCallback()
|
public int pttNativeSetPlayStopCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_Play_Stop_Callback("OEM_Play_Stop_cb");
|
try {
|
||||||
|
return OEM_Set_Play_Stop_Callback("OEM_Play_Stop_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetRecordStartCallback()
|
public int pttNativeSetRecordStartCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_Record_Start_Callback("OEM_Record_Start_cb");
|
try {
|
||||||
|
return OEM_Set_Record_Start_Callback("OEM_Record_Start_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetRecordStopCallback()
|
public int pttNativeSetRecordStopCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_Record_Stop_Callback("OEM_Record_Stop_cb");
|
try {
|
||||||
|
return OEM_Set_Record_Stop_Callback("OEM_Record_Stop_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetTTSCallback()
|
public int pttNativeSetTTSCallback()
|
||||||
{
|
{
|
||||||
return OEM_Set_TTS_Callback("OEM_Play_TTS_cb");
|
try {
|
||||||
|
return OEM_Set_TTS_Callback("OEM_Play_TTS_cb");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pttNativeSetPackageName(String name)
|
public int pttNativeSetPackageName(String name)
|
||||||
{
|
{
|
||||||
return OEM_Set_Package_Name(name);
|
try {
|
||||||
|
return OEM_Set_Package_Name(name);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public pttNative() {
|
public pttNative() {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,27 @@ class MyApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// 第一步:先初始化日志,哪怕后面的代码崩了,这里的初始化也完成了
|
// 第一步:初始化日志
|
||||||
LogManager.init(this)
|
LogManager.init(this)
|
||||||
|
|
||||||
|
// 【核心】初始化 xCrash 以捕获 Native (SO) 崩溃
|
||||||
|
xcrash.XCrash.init(this, xcrash.XCrash.InitParameters().apply {
|
||||||
|
setAppVersion(packageManager.getPackageInfo(packageName, 0).versionName)
|
||||||
|
setLogDir(getExternalFilesDir("tombstones")?.absolutePath)
|
||||||
|
setNativeDumpAllThreads(true)
|
||||||
|
|
||||||
|
// Java 崩溃回调
|
||||||
|
setJavaCallback { logPath, emergency ->
|
||||||
|
LogManager.onCrashDetected(logPath, "Java-xCrash", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native 崩溃回调
|
||||||
|
setNativeCallback { logPath, emergency ->
|
||||||
|
LogManager.onCrashDetected(logPath, "Native-xCrash", null)
|
||||||
|
android.util.Log.e("MyApplication", "NATIVE CRASH DETECTED at: $logPath")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,55 +61,58 @@ object LogManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专门供外部(如 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() {
|
private fun setupCrashHandler() {
|
||||||
// 1. 捕获所有普通线程、子线程的未捕获异常
|
// 1. 捕获所有线程
|
||||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
saveCrashToFile(thread, throwable)
|
onCrashDetected(null, thread.name, throwable)
|
||||||
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 捕获主线程 (UI 线程) 的所有异常
|
// 2. 捕获主线程 Looper
|
||||||
// 这种方式能捕获点击事件、生命周期回调中发生的所有崩溃
|
|
||||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
android.os.Looper.loop()
|
android.os.Looper.loop()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
saveCrashToFile(android.os.Looper.getMainLooper().thread, e)
|
onCrashDetected(null, "MainLooper", e)
|
||||||
// 如果你希望应用在崩溃后不闪退而是尝试恢复,可以不抛出,但建议这里还是交还给系统处理比较稳妥
|
// 如果是 UnsatisfiedLinkError,这种错误无法恢复,必须抛出让系统处理
|
||||||
// 这里我们为了记录完日志后让系统正常走关闭流程
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveCrashToFile(thread: Thread, throwable: Throwable) {
|
|
||||||
// 使用同步写入,确保磁盘 IO 优先
|
|
||||||
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
|
|
||||||
val crashInfo = "\n\nCRASH_ERROR_REPORT\n" +
|
|
||||||
"Time: $timestamp\n" +
|
|
||||||
"Thread: ${thread.name}\n" +
|
|
||||||
"Cause: ${throwable.cause}\n" +
|
|
||||||
"Message: ${throwable.message}\n" +
|
|
||||||
"Full Stacktrace:\n${throwable.stackTraceToString()}\n" +
|
|
||||||
"-------------------\n\n"
|
|
||||||
|
|
||||||
logDir?.let { dir ->
|
|
||||||
try {
|
|
||||||
val logFile = File(dir, SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + ".log")
|
|
||||||
val fos = FileOutputStream(logFile, true)
|
|
||||||
fos.write(crashInfo.toByteArray())
|
|
||||||
fos.flush()
|
|
||||||
fos.fd.sync()
|
|
||||||
fos.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("LogManager", "Failed to save crash log", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanOldLogs(dir: File) {
|
private fun cleanOldLogs(dir: File) {
|
||||||
executor.execute {
|
executor.execute {
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,15 +129,28 @@ object LogManager {
|
||||||
fun zipLogs(context: Context): String? {
|
fun zipLogs(context: Context): String? {
|
||||||
val dir = logDir ?: return null
|
val dir = logDir ?: return null
|
||||||
val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip")
|
val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip")
|
||||||
|
val tombstoneDir = File(context.getExternalFilesDir(null), "tombstones")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
||||||
|
// 1. 打包普通日志
|
||||||
dir.listFiles()?.forEach { file ->
|
dir.listFiles()?.forEach { file ->
|
||||||
if (file.isFile && file.name.endsWith(".log")) {
|
if (file.isFile && (file.name.endsWith(".log") || file.name.endsWith(".txt"))) {
|
||||||
zos.putNextEntry(ZipEntry(file.name))
|
zos.putNextEntry(ZipEntry("logs/${file.name}"))
|
||||||
FileInputStream(file).use { it.copyTo(zos) }
|
FileInputStream(file).use { it.copyTo(zos) }
|
||||||
zos.closeEntry()
|
zos.closeEntry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 2. 打包 Native 崩溃文件 (Tombstones)
|
||||||
|
if (tombstoneDir.exists()) {
|
||||||
|
tombstoneDir.listFiles()?.forEach { file ->
|
||||||
|
if (file.isFile) {
|
||||||
|
zos.putNextEntry(ZipEntry("native/${file.name}"))
|
||||||
|
FileInputStream(file).use { it.copyTo(zos) }
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
zipFile.absolutePath
|
zipFile.absolutePath
|
||||||
} catch (e: Exception) { null }
|
} catch (e: Exception) { null }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue