改进崩溃通知的各类源码作为类库适配能力。

This commit is contained in:
2025-11-30 16:29:37 +08:00
parent 8aafcabba9
commit ad6d29c27e
4 changed files with 151 additions and 124 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 08:19:07 GMT 2025
#Sun Nov 30 08:26:11 GMT 2025
stageCount=4
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.3
buildCount=3
buildCount=5
baseBetaVersion=15.11.4

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 08:19:07 GMT 2025
#Sun Nov 30 08:26:11 GMT 2025
stageCount=4
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.3
buildCount=3
buildCount=5
baseBetaVersion=15.11.4

View File

@@ -186,7 +186,7 @@ public final class CrashHandler {
);
try {
if (GlobalApplication.isDebugging()) {
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
// 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {

View File

@@ -7,6 +7,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.GlobalCrashActivity;
import cc.winboll.studio.libappbase.LogUtils;
@@ -14,8 +15,9 @@ import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集
* 核心功能:应用崩溃时捕获错误日志发送通知到系统通知栏3行内容省略点击通知跳转 GlobalCrashActivity 并传递日志
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
*/
public class CrashHandleNotifyUtils {
@@ -35,174 +37,205 @@ public class CrashHandleNotifyUtils {
private static final int NOTIFICATION_MAX_LINES = 3;
/**
* 处理未捕获异常(核心方法)
* 1. 提取应用名称和崩溃日志;
* 2. 创建并发送系统通知3行内容省略无复制按钮
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
* @param app 应用全局 Application 实例(用于获取上下文、应用信息
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
* 处理未捕获异常(核心方法,类库入口
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来
*/
public static void handleUncaughtException(Application app, Intent intent) {
// 1. 提取应用名称(优化:从 Application 中获取真实应用名,替代原类名
String appName = getAppName(app);
// 2. 提取崩溃日志(从 Intent Extra 中获取,对应 CrashHandler 存储的崩溃信息)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
// 校验参数(避免空指针,确保通知正常发送)
if (app == null || appName == null || errorLog == null) {
LogUtils.e(TAG, "发送崩溃通知失败参数为空app=" + app + ", appName=" + appName + ", errorLog=" + errorLog + "");
public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) {
// 1. 校验核心参数(类库场景必须严格校验,避免空指针
if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
LogUtils.e(TAG, "发送崩溃通知失败参数为空hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + "");
return;
}
// 3. 发送崩溃通知到通知栏3行省略点击跳转 GlobalCrashActivity
sendCrashNotification(app, appName, errorLog);
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆
String hostAppName = getHostAppName(hostApp, hostPackageName);
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
}
/**
* 获取应用真实名称(从 AndroidManifest 中读取 android:label
* 替代原 app.getClass().toString()(原方式获取的是类名,用户不可读)
* @param context 上下文(Application 实例
* @return 应用名称(读取失败返回 "未知应用"
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
* @param hostApp 宿主应用的 Application 实例
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志
*/
private static String getAppName(Context context) {
public static void handleUncaughtException(Application hostApp, Intent intent) {
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
if (TextUtils.isEmpty(hostPackageName)) {
hostPackageName = hostApp.getPackageName();
LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName);
}
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
// 调用核心方法处理
handleUncaughtException(hostApp, hostPackageName, errorLog);
}
/**
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @return 宿主应用名称(读取失败返回 "未知应用"
*/
private static String getHostAppName(Context hostContext, String hostPackageName) {
try {
// 从包管理器中获取应用信息(包含应用名称
return context.getPackageManager().getApplicationLabel(
context.getApplicationInfo()
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进
return hostContext.getPackageManager().getApplicationLabel(
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
).toString();
} catch (Exception e) {
LogUtils.e(TAG, "获取应用名称失败", e);
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + "", e);
return "未知应用";
}
}
/**
* 发送崩溃通知到系统通知栏(移除复制按钮,点击跳转 GlobalCrashActivity
* @param context 上下文Application 实例,确保后台也能发送)
* @param title 通知标题(应用名称
* @param content 通知内容(崩溃日志,需传递给 GlobalCrashActivity
* 发送崩溃通知到宿主系统通知栏(类库兼容版
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
* @param hostContext 宿主应用的上下文Application 实例
* @param hostPackageName 宿主应用的包名
* @param hostAppName 宿主应用的名称(用于通知标题)
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
*/
private static void sendCrashNotification(Context context, String title, String content) {
// 1. 获取通知管理器(系统服务,用于发送/管理通知
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空");
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + "");
return;
}
// 2. 适配 Android 8.0+API 26+):创建通知渠道(必须,否则通知不显示
// 2. 适配 Android 8.0+API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(notificationManager);
createCrashNotifyChannel(hostContext, notificationManager);
}
// 3. 构建通知意图(核心修改:点击跳转 GlobalCrashActivity,传递崩溃日志
PendingIntent jumpIntent = getGlobalCrashPendingIntent(context, content); // 跳转崩溃详情页意图
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
if (jumpIntent == null) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "");
return;
}
// 4. 构建通知实例(移除复制按钮仅保留3行内容省略
Notification notification = buildNotification(context, title, content, jumpIntent);
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
// 5. 发送通知(指定通知ID重复发送同ID会覆盖原通知
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆
notificationManager.notify(CRASH_NOTIFY_ID, notification);
LogUtils.d(TAG, "崩溃通知发送成功标题=" + title + "内容长度=" + content.length() + "字符(点击跳转崩溃详情页");
LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + "标题" + hostAppName + "日志长度" + errorLog.length() + "字符)");
}
/**
* 创建崩溃通知渠道(Android 8.0+ 必需
* 通知渠道用于归类通知,用户可在系统设置中管理(开启/关闭/静音)
* @param notificationManager 通知管理器
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突
* @param hostContext 宿主应用的上下文
* @param notificationManager 宿主的通知管理器
*/
private static void createCrashNotifyChannel(NotificationManager notificationManager) {
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
// 仅 Android 8.0+ 执行(避免低版本报错)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 构建通知渠道(指定ID、名称、重要性
// 构建通知渠道(归属宿主应用,描述明确类库用途
android.app.NotificationChannel channel = new android.app.NotificationChannel(
CRASH_NOTIFY_CHANNEL_ID,
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
NotificationManager.IMPORTANCE_DEFAULT
);
// 可选:设置渠道描述(用户在设置中可见)
channel.setDescription("用于显示应用崩溃信息,点击查看详情");
// 注册通知渠道到系统
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
notificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID);
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + "渠道ID" + CRASH_NOTIFY_CHANNEL_ID + "");
}
}
/**
* 核心改:构建跳转 GlobalCrashActivity 的意图(传递崩溃日志
* 与 GlobalCrashActivity 的日志接收键EXTRA_CRASH_INFO保持一致
* @param context 上下文
* @param errorLog 崩溃日志(需传递给 GlobalCrashActivity
* 核心改:构建跳转宿主 GlobalCrashActivity 的意图(类库关键
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
* @param hostContext 宿主应用的上下文
* @param hostPackageName 宿主应用的包名
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
* @return 跳转崩溃详情页的 PendingIntent
*/
private static PendingIntent getGlobalCrashPendingIntent(Context context, String errorLog) {
// 1. 构建跳转 GlobalCrashActivity 的显式意图
Intent crashIntent = new Intent(context, GlobalCrashActivity.class);
// 传递崩溃日志EXTRA_CRASH_INFOGlobalCrashActivity 一致)
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_INFO, errorLog);
// 设置意图标志:确保 Activity 正常启动,避免重复创建
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
try {
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity避免类库包名干扰
crashIntent.setPackage(hostPackageName);
// 传递崩溃日志EXTRA_CRASH_INFO与宿主 GlobalCrashActivity 完全匹配)
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_INFO, errorLog);
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 2. 构建 PendingIntent延迟执行的意图
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 适配 Android 12+API 31+):添加 FLAG_IMMUTABLE 避免安全警告
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE;
// 2. 构建 PendingIntent使用宿主上下文,适配高版本
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE;
}
return PendingIntent.getActivity(
hostContext,
CRASH_NOTIFY_ID, // 用通知ID作为请求码确保唯一避免意图复用
crashIntent,
flags
);
} catch (Exception e) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "", e);
return null;
}
return PendingIntent.getActivity(
context,
CRASH_NOTIFY_ID, // 用通知ID作为请求码确保唯一避免意图复用
crashIntent,
flags
);
}
/**
* 构建通知实例(核心调整移除复制按钮仅保留3行内容省略
* @param context 上下文
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志
* @param jumpIntent 通知点击跳转意图(跳转 GlobalCrashActivity
* 构建通知实例(类库兼容版
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
* @param hostContext 宿主应用的上下文
* @param hostAppName 宿主应用的名称(通知标题
* @param errorLog 崩溃日志(通知内容
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity
* @return 构建完成的 Notification 对象
*/
private static Notification buildNotification(Context context, String title, String content, PendingIntent jumpIntent) {
// 兼容 Android 8.0+指定通知渠道ID
Notification.Builder builder = new Notification.Builder(context);
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
// 兼容 Android 8.0+:指定宿主的通知渠道ID
Notification.Builder builder = new Notification.Builder(hostContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
}
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”(使用宿主上下文构建)
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); // 底部省略提示
bigTextStyle.bigText(content); // 完整日志(下拉时显示)
bigTextStyle.setBigContentTitle(title); // 下拉后的标题(与主标题一致)
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
bigTextStyle.bigText(errorLog);
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
builder.setStyle(bigTextStyle);
// 配置通知核心参数(移除复制按钮相关代码
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主
builder
.setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(必需,否则通知不显示
.setContentTitle(title + " 崩溃") // 优化标题:明确标识“崩溃”
.setContentText(getShortContent(content)) // 核心3行内缩略文本
.setContentIntent(jumpIntent) // 通知主体点击跳转 GlobalCrashActivity
.setAutoCancel(true) // 点击通知后自动取消
.setWhen(System.currentTimeMillis()) // 通知创建时间
.setPriority(Notification.PRIORITY_DEFAULT); // 通知优先级
// 关键:使用宿主应用的小图标(避免类库图标显示异常
.setSmallIcon(hostContext.getApplicationInfo().icon)
.setContentTitle(hostAppName + " 崩溃")
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
.setAutoCancel(true) // 点击后自动关闭
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT);
// 适配 Android 4.1+:确保文本显示正常
// 构建通知并返回(兼容低版本,区分 API 等级避免报错)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return builder.build();
} else {
// Android 4.0 及以下版本,使用 getNotification() 方法
return builder.getNotification();
}
// 适配 Android 4.1+:确保在宿主应用中正常显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return builder.build();
} else {
return builder.getNotification();
}
}
/**
* 辅助方法截取日志文本确保显示在3行内按字符数估算,适配大多数设备
* 一行约20-30字符3行约80字符留冗余取80字符超出加省略号
* 辅助方法截取日志文本确保显示在3行内通用逻辑,无包名依赖
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
@@ -210,22 +243,16 @@ public class CrashHandleNotifyUtils {
if (content == null || content.isEmpty()) {
return "无崩溃日志";
}
// 估算3行字符数80字符可根据设备屏幕调整避免因字符过长导致换行超3行
int maxLength = 80;
if (content.length() <= maxLength) {
return content; // 不足3行直接返回完整文本
} else {
// 超出3行截取前80字符并加省略号确保视觉上仅显示3行
return content.substring(0, maxLength) + "...";
}
int maxLength = 80; // 估算3行字符数可根据需求调整
return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
}
/**
* 释放资源(移除广播注销逻辑,保留空实现避免调用报错)
* @param context 上下文Application 实例
* 释放资源(类库场景:空实现避免宿主调用报错,预留扩展
* @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖
*/
public static void release(Context context) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成");
public static void release(Context hostContext) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + "");
}
}