@@ -6,15 +6,17 @@ import android.app.NotificationManager;
import android.app.PendingIntent ;
import android.content.Context ;
import android.content.Intent ;
import android.content.IntentFilter ;
import android.os.Build ;
import cc.winboll.studio.libappbase.CrashHandler ;
import cc.winboll.studio.libappbase.LogUtils ;
import cc.winboll.studio.libappbase.R ;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集
* 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏,方便用户查看崩溃信息
* 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏( 3行内容省略+复制按钮),点击复制按钮可将完整崩溃日志复制到剪贴板
*/
public class CrashHandleNotifyUtils {
@@ -30,11 +32,22 @@ public class CrashHandleNotifyUtils {
private static final int API_LEVEL_ANDROID_12 = 31 ;
/** PendingIntent.FLAG_IMMUTABLE 常量值( API 31+),避免依赖高版本 SDK */
private static final int FLAG_IMMUTABLE = 0x00000040 ;
/** 通知内容最大行数( 控制在3行, 超出部分省略) */
private static final int NOTIFICATION_MAX_LINES = 3 ;
/** 复制按钮 Action( 用于区分通知按钮点击事件) */
private static final String ACTION_COPY_CRASH_LOG = " cc.winboll.studio.action.COPY_CRASH_LOG " ;
/** 复制按钮请求码(区分多个 PendingIntent) */
private static final int REQUEST_CODE_COPY = 0x002 ;
/** 复制成功提示文本 */
private static final String COPY_SUCCESS_TIP = " 崩溃日志已复制到剪贴板 " ;
// 静态广播接收器(避免重复注册,确保崩溃后仍能接收点击事件)
private static CopyCrashLogReceiver sCopyReceiver ;
/**
* 处理未捕获异常(核心方法)
* 1. 提取应用名称和崩溃日志;
* 2. 创建并发送系统通知(标题:应用名称,内容:崩溃日志 ) ;
* 2. 创建并发送系统通知(3行内容省略+复制按钮 ) ;
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
* @param app 应用全局 Application 实例(用于获取上下文、应用信息)
* @param intent 存储崩溃信息的意图( extra 中携带崩溃日志)
@@ -51,7 +64,10 @@ public class CrashHandleNotifyUtils {
return ;
}
// 3. 发送崩溃通知到通知栏
// 3. 注册静态广播接收器(仅注册一次,确保崩溃后能接收点击事件)
registerCopyReceiver ( app ) ;
// 4. 发送崩溃通知到通知栏( 3行省略+复制按钮)
sendCrashNotification ( app , appName , errorLog ) ;
}
@@ -74,7 +90,7 @@ public class CrashHandleNotifyUtils {
}
/**
* 发送崩溃通知到系统通知栏
* 发送崩溃通知到系统通知栏( 核心修改: 3行内容+复制按钮)
* @param context 上下文( Application 实例,确保后台也能发送)
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
@@ -92,11 +108,12 @@ public class CrashHandleNotifyUtils {
createCrashNotifyChannel ( notificationManager ) ;
}
// 3. 构建通知意图(点击通知时可跳转,此处默认跳转应用主界面,可自定义 )
PendingIntent p endingIntent = getNotification PendingIntent ( context ) ;
// 3. 构建通知意图(点击通知跳转主界面 + 点击复制按钮复制日志 )
PendingIntent launchP endingIntent = getLaunch PendingIntent ( context ) ; // 主界面跳转意图
PendingIntent copyPendingIntent = getCopyPendingIntent ( context , content ) ; // 复制日志意图
// 4. 构建通知实例(兼容低版本,使用 Notification.Builder 构建 )
Notification notification = buildNotification ( context , title , content , p endingIntent) ;
// 4. 构建通知实例(核心修复: 3行内容省略+复制按钮, 修复setLines报错 )
Notification notification = buildNotification ( context , title , content , launchPendingIntent , copyP endingIntent) ;
// 5. 发送通知( 指定通知ID, 重复发送同ID会覆盖原通知)
notificationManager . notify ( CRASH_NOTIFY_ID , notification ) ;
@@ -118,7 +135,7 @@ public class CrashHandleNotifyUtils {
NotificationManager . IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
) ;
// 可选:设置渠道描述(用户在设置中可见)
channel . setDescription ( " 用于显示应用崩溃信息,帮助定位问题 " ) ;
channel . setDescription ( " 用于显示应用崩溃信息,支持复制日志 " ) ;
// 注册通知渠道到系统
notificationManager . createNotificationChannel ( channel ) ;
LogUtils . d ( TAG , " 崩溃通知渠道创建成功: " + CRASH_NOTIFY_CHANNEL_ID ) ;
@@ -126,12 +143,11 @@ public class CrashHandleNotifyUtils {
}
/**
* 构建通知点击意图(PendingIntent )
* 点击通知后跳转应用主界面(可根据需求修改为跳转崩溃日志详情页)
* 构建通知点击跳转 意图(跳转应用主界面 )
* @param context 上下文
* @return 封装好的 PendingIntent(用于通知点击跳转)
* @return 主界面跳转 PendingIntent
*/
private static PendingIntent getNotification PendingIntent ( Context context ) {
private static PendingIntent getLaunch PendingIntent ( Context context ) {
// 1. 获取应用主界面 Intent( 从包名启动默认 launcher Activity)
Intent launchIntent = context . getPackageManager ( ) . getLaunchIntentForPackage (
context . getPackageName ( )
@@ -141,55 +157,223 @@ public class CrashHandleNotifyUtils {
launchIntent = new Intent ( ) ;
}
// 2. 构建 PendingIntent( 延迟执行的意图, FLAG_UPDATE_CURRENT 表示更新已存在的意图)
// 2. 构建 PendingIntent( 延迟执行的意图)
int flags = PendingIntent . FLAG_UPDATE_CURRENT ;
// 适配 Android 12+( API 31+):添加 FLAG_IMMUTABLE 避免安全警告(用常量值替代高版本 API)
// 适配 Android 12+( API 31+):添加 FLAG_IMMUTABLE 避免安全警告
if ( Build . VERSION . SDK_INT > = API_LEVEL_ANDROID_12 ) {
flags | = FLAG_IMMUTABLE ;
}
return PendingIntent . getActivity (
context ,
0 , // 请求码(可忽略,用于区分多个 PendingIntent )
0 , // 请求码(可忽略)
launchIntent ,
flags
) ;
}
/**
* 构建通知实例(兼容 Android 4.0+ 所有版本 )
* 构建复制按钮意图(点击复制崩溃日志到剪贴板 )
* @param context 上下文
* @param title 通知标题
* @param content 通知内容
* @param pendingIntent 通知点击意图
* @param errorLog 崩溃日志(需要复制的内容)
* @return 复制日志 PendingIntent
*/
private static PendingIntent getCopyPendingIntent ( Context context , String errorLog ) {
// 1. 构建复制日志的隐式意图(指定 Action, 用于 BroadcastReceiver 接收)
Intent copyIntent = new Intent ( ACTION_COPY_CRASH_LOG ) ;
copyIntent . putExtra ( " EXTRA_CRASH_LOG " , errorLog ) ; // 携带崩溃日志
copyIntent . setPackage ( context . getPackageName ( ) ) ; // 限制仅当前应用接收,避免安全问题
// 2. 构建 PendingIntent( 使用广播类型, 崩溃后仍能触发)
int flags = PendingIntent . FLAG_UPDATE_CURRENT ;
if ( Build . VERSION . SDK_INT > = API_LEVEL_ANDROID_12 ) {
flags | = FLAG_IMMUTABLE ;
}
return PendingIntent . getBroadcast (
context ,
REQUEST_CODE_COPY , // 唯一请求码,区分主界面意图
copyIntent ,
flags
) ;
}
/**
* 注册静态广播接收器(仅注册一次,确保崩溃后能接收点击事件)
* 解决动态广播崩溃后被销毁的问题
* @param context 上下文( Application 实例)
*/
private static void registerCopyReceiver ( Context context ) {
// 避免重复注册(静态接收器仅注册一次)
if ( sCopyReceiver ! = null ) {
return ;
}
// 构建广播过滤器(仅接收复制日志的 Action)
IntentFilter filter = new IntentFilter ( ) ;
filter . addAction ( ACTION_COPY_CRASH_LOG ) ;
filter . setPriority ( IntentFilter . SYSTEM_HIGH_PRIORITY ) ;
// 初始化静态广播接收器
sCopyReceiver = new CopyCrashLogReceiver ( ) ;
// 注册广播(使用 Application 上下文,确保生命周期与应用一致)
context . registerReceiver ( sCopyReceiver , filter ) ;
LogUtils . d ( TAG , " 复制日志广播接收器注册成功 " ) ;
}
/**
* 静态广播接收器(处理复制按钮点击事件)
* 静态内部类避免内存泄漏,且崩溃后仍能接收系统发送的广播
*/
private static class CopyCrashLogReceiver extends android . content . BroadcastReceiver {
@Override
public void onReceive ( Context context , Intent intent ) {
// 验证 Action, 确保是复制日志的点击事件
if ( ACTION_COPY_CRASH_LOG . equals ( intent . getAction ( ) ) ) {
// 从意图中获取完整崩溃日志
String crashLog = intent . getStringExtra ( " EXTRA_CRASH_LOG " ) ;
if ( crashLog ! = null & & ! crashLog . isEmpty ( ) ) {
// 复制日志到剪贴板
copyTextToClipboard ( context , " 崩溃日志 " , crashLog ) ;
// 复制成功后显示提示(可选,提升用户体验)
showCopySuccessTip ( context ) ;
LogUtils . d ( TAG , " 崩溃日志复制成功,长度: " + crashLog . length ( ) + " 字符 " ) ;
} else {
LogUtils . e ( TAG , " 复制崩溃日志失败:日志为空 " ) ;
}
}
}
}
/**
* 复制文本到系统剪贴板(修复适配逻辑,确保全版本可用)
* @param context 上下文
* @param label 剪贴板文本标签(用户不可见,用于区分剪贴板内容)
* @param text 需要复制的文本(崩溃日志)
*/
private static void copyTextToClipboard ( Context context , String label , String text ) {
try {
// 适配 Android 11+( API 30+)剪贴板 API, 兼容低版本
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . R ) {
android . content . ClipboardManager clipboard = ( android . content . ClipboardManager ) context . getSystemService ( Context . CLIPBOARD_SERVICE ) ;
android . content . ClipData clipData = android . content . ClipData . newPlainText ( label , text ) ;
clipboard . setPrimaryClip ( clipData ) ;
} else {
// 低版本剪贴板 API( Android 10 及以下)
android . text . ClipboardManager clipboard = ( android . text . ClipboardManager ) context . getSystemService ( Context . CLIPBOARD_SERVICE ) ;
clipboard . setText ( text ) ;
}
} catch ( Exception e ) {
LogUtils . e ( TAG , " 复制文本到剪贴板失败 " , e ) ;
}
}
/**
* 显示复制成功提示( Toast, 提升用户体验)
* @param context 上下文
*/
private static void showCopySuccessTip ( Context context ) {
try {
// 若项目中 ToastUtils 支持后台显示,用 ToastUtils; 否则用系统 Toast
if ( cc . winboll . studio . libappbase . ToastUtils . isInited ( ) ) {
cc . winboll . studio . libappbase . ToastUtils . show ( COPY_SUCCESS_TIP ) ;
} else {
// 系统 Toast 适配(确保后台能显示)
android . widget . Toast . makeText ( context , COPY_SUCCESS_TIP , android . widget . Toast . LENGTH_SHORT ) . show ( ) ;
}
} catch ( Exception e ) {
LogUtils . e ( TAG , " 显示复制成功提示失败 " , e ) ;
}
}
/**
* 构建通知实例( 核心修复: 3行内容省略+复制按钮, 修复setLines报错)
* @param context 上下文
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
* @param launchPendingIntent 通知点击跳转意图
* @param copyPendingIntent 复制按钮点击意图
* @return 构建完成的 Notification 对象
*/
private static Notification buildNotification ( Context context , String title , String content , PendingIntent p endingIntent) {
private static Notification buildNotification ( Context context , String title , String content , PendingIntent launchPendingIntent , PendingIntent copyP endingIntent) {
// 兼容 Android 8.0+: 指定通知渠道ID
Notification . Builder builder = new Notification . Builder ( context ) ;
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . O ) {
builder . setChannelId ( CRASH_NOTIFY_CHANNEL_ID ) ;
}
// 配置通知核心参数
builder
. setSmallIcon ( context . getApplicationInfo ( ) . icon ) // 通知小图标(必需,从应用图标获取)
. setContentTitle ( title ) // 通知标题(应用名称)
. setContentText ( content ) // 通知内容(崩溃日志)
. setContentIntent ( pendingIntent ) // 通知点击意图
. setAutoCancel ( true ) // 点击通知后自动取消
. setWhen ( System . currentTimeMillis ( ) ) // 通知创建时间(当前时间)
. setPriority ( Notification . PRIORITY_DEFAULT ) ; // 通知优先级(默认)
// 可选:长文本适配(当崩溃日志过长时,显示完整文本)
if ( content . length ( ) > 100 ) { // 超过100字符时, 设置长文本样式
// 核心修复1: 用BigTextStyle控制“默认3行省略, 下拉显示完整”
Notification . BigTextStyle bigTextStyle = new Notification . BigTextStyle ( ) ;
bigTextStyle . bigText ( content ) ; // 显示完整崩溃日志
bigTextStyle . setSummaryText ( " 日志已省略,下拉查看完整内容 " ) ; // 底部省略提示
bigTextStyle . bigText ( content ) ; // 完整日志(下拉时显示)
bigTextStyle . setBigContentTitle ( title ) ; // 下拉后的标题(与主标题一致)
builder . setStyle ( bigTextStyle ) ;
// 核心修改2: 添加复制按钮( Android 4.1+ 支持通知按钮)
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . JELLY_BEAN ) {
// 复制按钮:自定义图标+文本+点击意图(确保图标存在)
builder . addAction (
R . drawable . ic_content_copy , // 自定义复制图标( 需确保drawable目录下存在, 否则替换为系统图标)
" 复制日志 " , // 按钮文本
copyPendingIntent // 按钮点击意图(绑定复制广播)
) ;
}
// 构建通知并返回( getNotification() 兼容低版本, build() 是 Android 4.1+ 方法 )
return Build . VERSION . SDK_INT > = Build . VERSION_CODES . JELLY_BEAN ? builder . build ( ) : builder . getNotification ( ) ;
// 配置通知核心参数( 移除setLines, 避免报错 )
builder
. setSmallIcon ( context . getApplicationInfo ( ) . icon ) // 通知小图标(必需,否则通知不显示)
. setContentTitle ( title ) // 通知主标题(应用名称)
. setContentText ( getShortContent ( content ) ) // 核心: 3行内缩略文本
. setContentIntent ( launchPendingIntent ) // 通知主体点击跳转主界面
. 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 ( ) ;
}
}
/**
* 辅助方法: 截取日志文本, 确保显示在3行内( 按字符数估算, 适配大多数设备)
* 一行约20-30字符, 3行约80字符( 留冗余, 取80字符, 超出加省略号)
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
private static String getShortContent ( String content ) {
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 ) + " ... " ;
}
}
/**
* 释放资源(可选,在 Application 销毁时调用,避免内存泄漏)
* @param context 上下文( Application 实例)
*/
public static void release ( Context context ) {
// 注销静态广播接收器,避免内存泄漏
if ( sCopyReceiver ! = null & & context ! = null ) {
try {
context . unregisterReceiver ( sCopyReceiver ) ;
sCopyReceiver = null ;
LogUtils . d ( TAG , " 复制日志广播接收器已注销 " ) ;
} catch ( Exception e ) {
LogUtils . e ( TAG , " 注销广播接收器失败 " , e ) ;
}
}
}
}