20251130_161248_108

This commit is contained in:
2025-11-30 16:12:56 +08:00
parent e4dc8109aa
commit fc34ed0d5a
3 changed files with 18 additions and 167 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Nov 29 21:41:11 HKT 2025
#Sun Nov 30 08:12:09 GMT 2025
stageCount=4
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.3
buildCount=0
buildCount=2
baseBetaVersion=15.11.4

View File

@@ -6,17 +6,15 @@ 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行内容省略+复制按钮),点击复制按钮可将完整崩溃日志复制到剪贴板
* 核心功能应用崩溃时捕获错误日志发送通知到系统通知栏3行内容省略),点击通知跳转应用主界面
*/
public class CrashHandleNotifyUtils {
@@ -34,20 +32,11 @@ public class CrashHandleNotifyUtils {
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. 创建并发送系统通知3行内容省略+复制按钮);
* 2. 创建并发送系统通知3行内容省略,无复制按钮);
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
* @param app 应用全局 Application 实例(用于获取上下文、应用信息)
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
@@ -64,10 +53,7 @@ public class CrashHandleNotifyUtils {
return;
}
// 3. 注册静态广播接收器(仅注册一次,确保崩溃后能接收点击事件
registerCopyReceiver(app);
// 4. 发送崩溃通知到通知栏3行省略+复制按钮)
// 3. 发送崩溃通知到通知栏3行省略无复制按钮
sendCrashNotification(app, appName, errorLog);
}
@@ -90,7 +76,7 @@ public class CrashHandleNotifyUtils {
}
/**
* 发送崩溃通知到系统通知栏(核心修改3行内容+复制按钮
* 发送崩溃通知到系统通知栏(移除复制按钮仅显示3行内容
* @param context 上下文Application 实例,确保后台也能发送)
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
@@ -108,12 +94,11 @@ public class CrashHandleNotifyUtils {
createCrashNotifyChannel(notificationManager);
}
// 3. 构建通知意图(点击通知跳转主界面 + 点击复制按钮复制日志
// 3. 构建通知意图(仅保留:点击通知跳转主界面)
PendingIntent launchPendingIntent = getLaunchPendingIntent(context); // 主界面跳转意图
PendingIntent copyPendingIntent = getCopyPendingIntent(context, content); // 复制日志意图
// 4. 构建通知实例(核心修复3行内容省略+复制按钮修复setLines报错
Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent);
// 4. 构建通知实例(移除复制按钮仅保留3行内容省略
Notification notification = buildNotification(context, title, content, launchPendingIntent);
// 5. 发送通知指定通知ID重复发送同ID会覆盖原通知
notificationManager.notify(CRASH_NOTIFY_ID, notification);
@@ -135,7 +120,7 @@ public class CrashHandleNotifyUtils {
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
);
// 可选:设置渠道描述(用户在设置中可见)
channel.setDescription("用于显示应用崩溃信息,支持复制日志");
channel.setDescription("用于显示应用崩溃信息");
// 注册通知渠道到系统
notificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID);
@@ -173,153 +158,28 @@ public class CrashHandleNotifyUtils {
}
/**
* 构建复制按钮意图(点击复制崩溃日志到剪贴板
* @param context 上下文
* @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 {
// 低版本剪贴板 APIAndroid 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报错
* 构建通知实例核心调整移除复制按钮仅保留3行内容省略
* @param context 上下文
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
* @param launchPendingIntent 通知点击跳转意图
* @param copyPendingIntent 复制按钮点击意图
* @return 构建完成的 Notification 对象
*/
private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent, PendingIntent copyPendingIntent) {
private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent) {
// 兼容 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);
}
// 核心修复1用BigTextStyle控制“默认3行省略下拉显示完整”
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
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 // 按钮点击意图(绑定复制广播)
);
}
// 配置通知核心参数移除setLines避免报错
// 配置通知核心参数(移除复制按钮相关代码
builder
.setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(必需,否则通知不显示)
.setContentTitle(title) // 通知主标题(应用名称)
@@ -360,20 +220,11 @@ public class CrashHandleNotifyUtils {
}
/**
* 释放资源(可选,在 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);
}
}
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成");
}
}