Add ANR (Application Not Responding) tracking support

- Add isAnr flag to AppErrorsInfoBean data model
- Add cloneAnr() method to create ANR error records from ApplicationErrorReport.AnrInfo
- Hook appNotResponding() and handleAnrInActivityController() methods in framework
- Add ANR-specific error titles in all supported languages (EN, ZH-CN, ZH-HK, ZH-MO, ZH-TW, JA)
- Update error type display to show "ANR" for ANR errors
- Add handleAppAnrInfo() method to record ANR data

Co-authored-by: NextAlone <12210746+NextAlone@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-15 19:05:03 +00:00
parent fd2c563089
commit 94bc3dc066
8 changed files with 94 additions and 3 deletions

View File

@@ -50,6 +50,7 @@ import java.util.Locale
* @param targetSdk 目标 SDK 版本 * @param targetSdk 目标 SDK 版本
* @param minSdk 最低 SDK 版本 * @param minSdk 最低 SDK 版本
* @param isNativeCrash 是否为原生层异常 * @param isNativeCrash 是否为原生层异常
* @param isAnr 是否为 ANR (Application Not Responding)
* @param exceptionClassName 异常类名 * @param exceptionClassName 异常类名
* @param exceptionMessage 异常信息 * @param exceptionMessage 异常信息
* @param throwClassName 抛出异常的类名 * @param throwClassName 抛出异常的类名
@@ -78,6 +79,8 @@ data class AppErrorsInfoBean(
var minSdk: Int = -1, var minSdk: Int = -1,
@SerializedName("isNativeCrash") @SerializedName("isNativeCrash")
var isNativeCrash: Boolean = false, var isNativeCrash: Boolean = false,
@SerializedName("isAnr")
var isAnr: Boolean = false,
@SerializedName("exceptionClassName") @SerializedName("exceptionClassName")
var exceptionClassName: String = "", var exceptionClassName: String = "",
@SerializedName("exceptionMessage") @SerializedName("exceptionMessage")
@@ -134,6 +137,37 @@ data class AppErrorsInfoBean(
timestamp = System.currentTimeMillis() timestamp = System.currentTimeMillis()
) )
} }
/**
* 从 [ApplicationErrorReport.AnrInfo] 克隆
* @param context 当前实例
* @param pid APP 进程 ID
* @param userId APP 用户 ID
* @param packageName APP 包名
* @param anrInfo [ApplicationErrorReport.AnrInfo]
* @return [AppErrorsInfoBean]
*/
fun cloneAnr(context: Context, pid: Int, userId: Int, packageName: String?, anrInfo: ApplicationErrorReport.AnrInfo?) =
AppErrorsInfoBean(
pid = pid,
userId = userId,
cpuAbi = packageName?.let { context.appCpuAbiOf(it) } ?: "",
packageName = packageName ?: "unknown",
versionName = packageName?.let { context.appVersionNameOf(it).ifBlank { "unknown" } } ?: "",
versionCode = packageName?.let { context.appVersionCodeOf(it) } ?: -1L,
targetSdk = packageName?.let { context.appTargetSdkOf(it) } ?: -1,
minSdk = packageName?.let { context.appMinSdkOf(it) } ?: -1,
isNativeCrash = false,
isAnr = true,
exceptionClassName = "ANR",
exceptionMessage = anrInfo?.cause ?: "Application Not Responding",
throwFileName = anrInfo?.activity?.className ?: "unknown",
throwClassName = anrInfo?.activity?.packageName ?: packageName ?: "unknown",
throwMethodName = "unknown",
throwLineNumber = -1,
stackTrace = anrInfo?.info?.trim() ?: "unknown",
timestamp = System.currentTimeMillis()
)
} }
/** /**
@@ -248,7 +282,11 @@ data class AppErrorsInfoBean(
[Version Code]: ${versionCode.takeIf { it != -1L } ?: "unknown"} [Version Code]: ${versionCode.takeIf { it != -1L } ?: "unknown"}
[Target SDK]: ${targetSdk.takeIf { it != -1 } ?: "unknown"} [Target SDK]: ${targetSdk.takeIf { it != -1 } ?: "unknown"}
[Min SDK]: ${minSdk.takeIf { it != -1 } ?: "unknown"} [Min SDK]: ${minSdk.takeIf { it != -1 } ?: "unknown"}
[Error Type]: ${if (isNativeCrash) "Native" else "JVM"} [Error Type]: ${when {
isAnr -> "ANR"
isNativeCrash -> "Native"
else -> "JVM"
}}
[Crash Time]: $utcTime [Crash Time]: $utcTime
[Stack Trace]: [Stack Trace]:
""".trimIndent() + "\n$stackTrace" """.trimIndent() + "\n$stackTrace"

View File

@@ -98,8 +98,9 @@ object FrameworkHooker : YukiBaseHooker() {
* @param errors [AppErrorsClass] 实例 * @param errors [AppErrorsClass] 实例
* @param proc [ProcessRecordClass] 实例 * @param proc [ProcessRecordClass] 实例
* @param resultData [AppErrorDialog_DataClass] 实例 - 默认空 * @param resultData [AppErrorDialog_DataClass] 实例 - 默认空
* @param isAnr 是否为 ANR - 默认 false
*/ */
private class AppErrorsProcessData(errors: Any?, proc: Any?, resultData: Any? = null) { private class AppErrorsProcessData(errors: Any?, proc: Any?, resultData: Any? = null, val isAnr: Boolean = false) {
/** /**
* 获取当前包列表实例 * 获取当前包列表实例
@@ -330,7 +331,12 @@ object FrameworkHooker : YukiBaseHooker() {
val appNameWithUserId = if (userId != 0) "$appName (${locale.userId(userId)})" else appName val appNameWithUserId = if (userId != 0) "$appName (${locale.userId(userId)})" else appName
/** 崩溃标题 */ /** 崩溃标题 */
val errorTitle = if (isRepeatingCrash) locale.aerrRepeatedTitle(appNameWithUserId) else locale.aerrTitle(appNameWithUserId) val errorTitle = when {
isAnr && isRepeatingCrash -> locale.anrRepeatedTitle(appNameWithUserId)
isAnr -> locale.anrTitle(appNameWithUserId)
isRepeatingCrash -> locale.aerrRepeatedTitle(appNameWithUserId)
else -> locale.aerrTitle(appNameWithUserId)
}
/** 使用通知推送异常信息 */ /** 使用通知推送异常信息 */
fun showAppErrorsWithNotify() = fun showAppErrorsWithNotify() =
@@ -408,6 +414,16 @@ object FrameworkHooker : YukiBaseHooker() {
YLog.info("Received crash application data${if (userId != 0) " --user $userId" else ""} --pid $pid") YLog.info("Received crash application data${if (userId != 0) " --user $userId" else ""} --pid $pid")
} }
/**
* 处理 APP 进程 ANR 数据
* @param context 当前实例
* @param info ANR 错误报告数据实例
*/
private fun AppErrorsProcessData.handleAppAnrInfo(context: Context, info: ApplicationErrorReport.AnrInfo?) {
AppErrorsRecordData.add(AppErrorsInfoBean.cloneAnr(context, pid, userId, appInfo?.packageName, info))
YLog.info("Received ANR application data${if (userId != 0) " --user $userId" else ""} --pid $pid")
}
override fun onHook() { override fun onHook() {
/** 注册生命周期 */ /** 注册生命周期 */
registerLifecycle() registerLifecycle()
@@ -502,6 +518,31 @@ object FrameworkHooker : YukiBaseHooker() {
/** 创建 APP 进程异常数据类 */ /** 创建 APP 进程异常数据类 */
AppErrorsProcessData(instance, proc).handleAppErrorsInfo(context, args(index = 1).cast()) AppErrorsProcessData(instance, proc).handleAppErrorsInfo(context, args(index = 1).cast())
} }
/** Hook ANR handling methods */
firstMethodOrNull {
name = "appNotResponding"
}?.hook()?.after {
/** 当前实例 */
val context = appContext ?: firstFieldOrNull { name = "mContext" }?.of(instance)?.get<Context>() ?: return@after
/** 当前进程信息 - 第一个参数是 ProcessRecord */
val proc = args().first().any() ?: return@after YLog.warn("Received ANR but got null ProcessRecord")
/** 创建 APP 进程异常数据类并展示 ANR UI */
AppErrorsProcessData(instance, proc, isAnr = true).handleShowAppErrorUi(context)
}
firstMethodOrNull {
name = "handleAnrInActivityController"
returnType = Boolean::class
}?.hook()?.after {
/** 当前实例 */
val context = appContext ?: firstFieldOrNull { name = "mContext" }?.of(instance)?.get<Context>() ?: return@after
/** 当前进程信息 */
val proc = args().first().any() ?: return@after YLog.warn("Received ANR but got null ProcessRecord")
/** 创建 ANR 数据 - args(1) 应该包含 AnrInfo */
AppErrorsProcessData(instance, proc, isAnr = true).handleAppAnrInfo(context, args(index = 1).cast())
}
} }
} }
} }

View File

@@ -10,6 +10,8 @@
<string name="close_app">アプリを閉じる</string> <string name="close_app">アプリを閉じる</string>
<string name="aerr_title">%1$s が停止しました</string> <string name="aerr_title">%1$s が停止しました</string>
<string name="aerr_repeated_title">%1$s が繰り返し停止しています</string> <string name="aerr_repeated_title">%1$s が繰り返し停止しています</string>
<string name="anr_title">%1$s は応答していません</string>
<string name="anr_repeated_title">%1$s は繰り返し応答していません</string>
<string name="mute_if_unlock_tip">デバイスのロックが解除されるまで「%1$s」のエラーをミュートにします</string> <string name="mute_if_unlock_tip">デバイスのロックが解除されるまで「%1$s」のエラーをミュートにします</string>
<string name="mute_if_restart_tip">デバイスが再起動されるまで「%1$s」のエラーをミュートにします</string> <string name="mute_if_restart_tip">デバイスが再起動されるまで「%1$s」のエラーをミュートにします</string>
<string name="back">戻る</string> <string name="back">戻る</string>

View File

@@ -10,6 +10,8 @@
<string name="close_app">关闭应用</string> <string name="close_app">关闭应用</string>
<string name="aerr_title">%1$s 已停止运行</string> <string name="aerr_title">%1$s 已停止运行</string>
<string name="aerr_repeated_title">%1$s 屡次停止运行</string> <string name="aerr_repeated_title">%1$s 屡次停止运行</string>
<string name="anr_title">%1$s 无响应</string>
<string name="anr_repeated_title">%1$s 屡次无响应</string>
<string name="mute_if_unlock_tip">忽略“%1$s”的错误直到设备重新解锁</string> <string name="mute_if_unlock_tip">忽略“%1$s”的错误直到设备重新解锁</string>
<string name="mute_if_restart_tip">忽略“%1$s”的错误直到设备重新启动</string> <string name="mute_if_restart_tip">忽略“%1$s”的错误直到设备重新启动</string>
<string name="back">返回</string> <string name="back">返回</string>

View File

@@ -10,6 +10,8 @@
<string name="close_app">結束程式</string> <string name="close_app">結束程式</string>
<string name="aerr_title">%1$s 已停止運作</string> <string name="aerr_title">%1$s 已停止運作</string>
<string name="aerr_repeated_title">%1$s 屢次停止運作</string> <string name="aerr_repeated_title">%1$s 屢次停止運作</string>
<string name="anr_title">%1$s 无响应</string>
<string name="anr_repeated_title">%1$s 屡次无响应</string>
<string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到設備重新開屏</string> <string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到設備重新開屏</string>
<string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到設備重新開機</string> <string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到設備重新開機</string>
<string name="back">回退</string> <string name="back">回退</string>

View File

@@ -10,6 +10,8 @@
<string name="close_app">結束程式</string> <string name="close_app">結束程式</string>
<string name="aerr_title">%1$s 已停止運作</string> <string name="aerr_title">%1$s 已停止運作</string>
<string name="aerr_repeated_title">%1$s 屢次停止運作</string> <string name="aerr_repeated_title">%1$s 屢次停止運作</string>
<string name="anr_title">%1$s 无响应</string>
<string name="anr_repeated_title">%1$s 屡次无响应</string>
<string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到設備重新開屏</string> <string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到設備重新開屏</string>
<string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到設備重新開機</string> <string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到設備重新開機</string>
<string name="back">回退</string> <string name="back">回退</string>

View File

@@ -10,6 +10,8 @@
<string name="close_app">結束程式</string> <string name="close_app">結束程式</string>
<string name="aerr_title">%1$s 已停止運作</string> <string name="aerr_title">%1$s 已停止運作</string>
<string name="aerr_repeated_title">%1$s 屢次停止運作</string> <string name="aerr_repeated_title">%1$s 屢次停止運作</string>
<string name="anr_title">%1$s 无响应</string>
<string name="anr_repeated_title">%1$s 屡次无响应</string>
<string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到裝置重新展示</string> <string name="mute_if_unlock_tip">忽略“%1$s”的錯誤直到裝置重新展示</string>
<string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到裝置重新開機</string> <string name="mute_if_restart_tip">忽略“%1$s”的錯誤直到裝置重新開機</string>
<string name="back">回退</string> <string name="back">回退</string>

View File

@@ -10,6 +10,8 @@
<string name="close_app">Close App</string> <string name="close_app">Close App</string>
<string name="aerr_title">%1$s has stopped</string> <string name="aerr_title">%1$s has stopped</string>
<string name="aerr_repeated_title">%1$s keeps stopping</string> <string name="aerr_repeated_title">%1$s keeps stopping</string>
<string name="anr_title">%1$s isn\'t responding</string>
<string name="anr_repeated_title">%1$s keeps not responding</string>
<string name="mute_if_unlock_tip">Muted errors for \'%1$s\' until device is re-unlocked</string> <string name="mute_if_unlock_tip">Muted errors for \'%1$s\' until device is re-unlocked</string>
<string name="mute_if_restart_tip">Muted errors for \'%1$s\' until device reboots</string> <string name="mute_if_restart_tip">Muted errors for \'%1$s\' until device reboots</string>
<string name="back">Back</string> <string name="back">Back</string>