@@ -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 a pp, 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 hostA pp, 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 ( Co ntex t co ntex t) {
public static void handleUncaughtException ( Application hostApp , I nten t i nten t) {
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 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 c ontext. getPackageManager ( ) . getApplicationLabel (
c ontext. getApplicationInfo ( )
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进 )
return hostC ontext. getPackageManager ( ) . getApplicationLabel (
hostC ontext. 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 c ontext, String titl e, String content ) {
// 1. 获取通知管理器(系统服务,用于发送/管理通知 )
NotificationManager notificationManager = ( NotificationManager ) c ontext. getSystemService ( Context . NOTIFICATION_SERVICE ) ;
private static void sendCrashNotification ( Context hostC ontext, String hostPackageNam e, String hostAppName , String errorLog ) {
// 1. 获取宿主的 通知管理器(使用宿主上下文,确保通知归属宿主应用 )
NotificationManager notificationManager = ( NotificationManager ) hostC ontext. 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 ( c ontext, content ) ; // 跳转崩溃详情页意图
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
PendingIntent jumpIntent = getGlobalCrashPendingIntent ( hostC ontext, hostPackageName , errorLog ) ;
if ( jumpIntent = = null ) {
LogUtils . e ( TAG , " 构建跳转意图失败(宿主包名: " + hostPackageName + " ) " ) ;
return ;
}
// 4. 构建通知实例(移除复制按钮, 仅保留3行内容省略 )
Notification notification = buildNotification ( c ontext, titl e, content , jumpIntent ) ;
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主 )
Notification notification = buildNotification ( hostC ontext, hostAppNam e, errorLog , jumpIntent ) ;
// 5. 发送通知(指定通知ID, 重复发送同ID会覆盖原通知 )
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆 )
notificationManager . notify ( CRASH_NOTIFY_ID , notification ) ;
LogUtils . d ( TAG , " 崩溃通知发送成功: 标题= " + titl e + " , 内容 长度= " + content . length ( ) + " 字符(点击跳转崩溃详情页 ) " ) ;
LogUtils . d ( TAG , " 崩溃通知发送成功(宿主包名: " + hostPackageName + " , 标题: " + hostAppNam e + " , 日志 长度: " + 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 c ontext, String errorLog ) {
// 1. 构建跳转 GlobalCrashActivity 的显式意图
Intent crashIntent = new Intent ( context , GlobalCrashActivity . class ) ;
// 传递崩溃日志( 键: EXTRA_CRASH_INFO, 与 GlobalCrashActivity 一致)
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 hostC ontext, 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 c ontext, String titl e, String content , PendingIntent jumpIntent ) {
// 兼容 Android 8.0+: 指定通知渠道ID
Notification . Builder builder = new Notification . Builder ( c ontext) ;
private static Notification buildNotification ( Context hostC ontext, String hostAppNam e, String errorLog , PendingIntent jumpIntent ) {
// 兼容 Android 8.0+:指定宿主的 通知渠道ID
Notification . Builder builder = new Notification . Builder ( hostC ontext) ;
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行内缩略文本
. setContentInten t ( jumpIntent ) // 通知主体点击跳转 GlobalCrashActivity
. setAutoCancel ( true ) // 点击通知后自动取消
. setWhen ( System . currentTimeMillis ( ) ) // 通知创建时间
. setPriority ( Notification . PRIORITY_DEFAULT ) ; // 通知优先级
// 关键:使用宿主应用的小图标(避免类库图标显示异常 )
. setSmallIcon ( hostContext . getApplicationInfo ( ) . icon )
. setContentTitle ( hostAppName + " 崩溃 " )
. setContentTex t ( getShortContent ( errorLog ) ) // 3行内缩略文本
. setContentIntent ( jumpIntent ) // 点击跳转宿主的 GlobalCrashActivity
. setAutoCancel ( true ) // 点击后自动关闭
. setWhen ( System . currentTimeMillis ( ) )
. setPriority ( Notification . PRIORITY_DEFAULT ) ;
// 适配 Android 4.1+:确保文本显示正常
// 构建通知并返回(兼容低版本,区分 API 等级避免报错)
if ( B uild. 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 b uilder . 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 c ontext 上下文( Application 实例 )
* 释放资源(类库场景: 空实现, 避免宿主 调用时 报错,预留扩展 )
* @param hostC ontext 宿主应用的上下文(显式传入,避免类库上下文依赖 )
*/
public static void release ( Context c ontext) {
LogUtils . d ( TAG , " CrashHandleNotifyUtils 资源释放完成 " ) ;
public static void release ( Context hostC ontext) {
LogUtils . d ( TAG , " CrashHandleNotifyUtils 资源释放完成(宿主包名: " + ( hostContext ! = null ? hostContext . getPackageName ( ) : " 未知 " ) + " ) " ) ;
}
}