完成应用崩溃事务处理逻辑。

This commit is contained in:
2026-05-11 16:53:51 +08:00
parent bd01220892
commit 744fb23291
7 changed files with 452 additions and 576 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon May 11 08:36:26 GMT 2026
#Mon May 11 08:50:12 GMT 2026
stageCount=6
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.5
buildCount=21
buildCount=26
baseBetaVersion=15.20.6

View File

@@ -22,7 +22,9 @@ import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -37,342 +39,292 @@ import java.util.Date;
import java.util.Locale;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:14
* @Describe * 应用全局崩溃处理类(单例逻辑)
* 应用全局崩溃处理类(单例逻辑)
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025/11/11 20:14:00
* @EditTime 2026/05/11 15:36:45
*/
public final class CrashHandler {
/** 日志标签,用于当前类的日志输出标识 */
public static final String TAG = "CrashHandler";
// ====================== 常量定义 ======================
/** 日志标签 */
public static final String TAG = "CrashHandler";
/** 崩溃报告页面标题 */
public static final String TITTLE = "CrashReport";
/** Intent 传递崩溃信息键 */
public static final String EXTRA_CRASH_LOG = "crashInfo";
/** SharedPreferences 存储键 */
static final String PREFS = CrashHandler.class.getName() + "PREFS";
/** 标记是否发生崩溃键 */
static final String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
/** 崩溃报告页面标题 */
public static final String TITTLE = "CrashReport";
// ====================== 成员变量 ======================
/** 崩溃保险丝状态文件路径 */
public static String _CrashCountFilePath;
/** 系统默认异常处理器兜底 */
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER
= Thread.getDefaultUncaughtExceptionHandler();
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
public static final String EXTRA_CRASH_LOG = "crashInfo";
// ====================== 对外初始化方法 ======================
/**
* 初始化崩溃处理器(默认存储路径)
* @param app 全局Application实例
*/
public static void init(final Application app) {
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
LogUtils.d(TAG, "init _CrashCountFilePath = " + _CrashCountFilePath);
init(app, null);
}
/** SharedPreferences 存储键(用于记录崩溃状态) */
final static String PREFS = CrashHandler.class.getName() + "PREFS";
/** SharedPreferences 中存储「是否发生崩溃」的键 */
final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
/** 崩溃保险丝状态文件路径(存储当前熔断等级) */
public static String _CrashCountFilePath;
/** 系统默认的未捕获异常处理器(用于降级处理,避免 CrashHandler 自身崩溃) */
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
/**
* 初始化崩溃处理器(默认存储路径)
* 调用重载方法,崩溃日志默认存储在应用外部私有目录的 crash 文件夹下
* @param app 全局 Application 实例(用于获取存储目录、包信息等)
*/
public static void init(Application app) {
// 初始化崩溃保险丝状态文件路径(外部存储/CrashHandler/IsCrashHandlerCrashHappen.dat
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
// 调用带目录参数的初始化方法,传入 null 使用默认路径
init(app, null);
}
/**
* 初始化崩溃处理器(指定日志存储目录)
* 替换系统默认的未捕获异常处理器,自定义崩溃处理逻辑
* @param app 全局 Application 实例
* @param crashDir 崩溃日志存储目录null 则使用默认路径)
*/
public static void init(final Application app, final String crashDir) {
// 设置自定义未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
/**
* 初始化崩溃处理器(自定义日志目录)
* @param app 全局Application实例
* @param crashDir 自定义崩溃日志目录传null使用默认
*/
public static void init(final Application app, final String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
public void uncaughtException(final Thread thread, final Throwable throwable) {
try {
// 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
tryUncaughtException(thread, throwable);
tryUncaughtException(thread, throwable, crashDir, app);
} catch (Throwable e) {
e.printStackTrace();
// 处理失败时,交给系统默认处理器兜底
LogUtils.e(TAG, "uncaughtException error", e);
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
}
/**
* 实际处理崩溃的核心方法
* 1. 熔断保险丝记录崩溃次数2. 收集崩溃信息3. 写入日志文件4. 启动崩溃报告页面
* @param thread 发生崩溃的线程
* @param throwable 崩溃异常对象(包含堆栈信息)
*/
private void tryUncaughtException(Thread thread, Throwable throwable) {
// 触发崩溃保险丝(每次崩溃熔断一次,降低防护等级)
AppCrashSafetyWire.getInstance().burnSafetyWire();
// 格式化崩溃发生时间(用于日志文件名和内容)
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
// 创建崩溃日志文件(默认路径:外部存储/crash/[时间].txt
File crashFile = new File(
TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash") : new File(crashDir),
"crash_" + time + ".txt"
);
// 获取应用版本信息(版本名、版本号)
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
versionName = packageInfo.versionName;
// 适配 Android 9.0+API 28的版本号获取方式
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException ignored) {}
// 将异常堆栈信息转换为字符串
String fullStackTrace;
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw); // 将异常堆栈写入 PrintWriter
fullStackTrace = sw.toString();
pw.close();
}
// 拼接崩溃信息(设备信息 + 应用信息 + 堆栈信息)
StringBuilder sb = new StringBuilder();
sb.append("************* Crash Head ****************\n");
sb.append("Time Of Crash : ").append(time).append("\n");
sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n"); // 设备厂商
sb.append("Device Model : ").append(Build.MODEL).append("\n"); // 设备型号
sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n"); // Android 版本
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n"); // SDK 版本
sb.append("App VersionName : ").append(versionName).append("\n"); // 应用版本名
sb.append("App VersionCode : ").append(versionCode).append("\n"); // 应用版本号
sb.append("************* Crash Head ****************\n");
sb.append("\n").append(fullStackTrace); // 拼接异常堆栈
final String errorLog = sb.toString();
// 将崩溃日志写入文件(忽略写入失败)
try {
writeFile(crashFile, errorLog);
} catch (IOException ignored) {}
// 启动崩溃报告页面(标签用于代码块折叠)
gotoCrashActiviy: {
Intent intent = new Intent();
LogUtils.d(TAG, "gotoCrashActiviy: ");
// 根据保险丝状态选择启动的崩溃页面
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
// 保险丝正常启动自定义样式的崩溃报告页面GlobalCrashActivity
intent.setClass(app, GlobalCrashActivity.class);
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
} else {
LogUtils.d(TAG, "gotoCrashActiviy: else");
// 保险丝熔断启动基础版崩溃页面CrashActivity避免复杂页面再次崩溃
intent.setClass(app, CrashActivity.class);
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
}
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
);
try {
if (GlobalApplication.isDebugging()) {
// 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {
// 发送一个通知
CrashHandleNotifyUtils.handleUncaughtException(app, intent, GlobalCrashActivity.class);
}
// 终止当前进程(确保完全重启)
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (ActivityNotFoundException e) {
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
e.printStackTrace();
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
} catch (Exception e) {
// 其他异常,兜底处理
e.printStackTrace();
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
}
}
/**
* 将字符串内容写入文件(创建父目录、覆盖写入)
* @param file 目标文件(包含路径)
* @param content 待写入的内容(崩溃日志)
* @throws IOException 文件创建或写入失败时抛出
*/
private void writeFile(File file, String content) throws IOException {
File parentFile = file.getParentFile();
// 父目录不存在则创建
if (parentFile != null && !parentFile.exists()) {
parentFile.mkdirs();
}
file.createNewFile(); // 创建文件
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes()); // 写入内容(默认 UTF-8 编码)
try {
fos.close(); // 关闭流
} catch (IOException e) {}
}
});
}
}
/**
* 基础版崩溃报告页面(保险丝熔断时启动)
* 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
*/
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
/** 菜单标识:复制崩溃日志 */
private static final int MENUITEM_COPY = 0;
/** 菜单标识:重启应用 */
private static final int MENUITEM_RESTART = 1;
// ====================== 内部崩溃处理核心 ======================
/**
* 执行崩溃信息收集、日志写入、跳转崩溃页面
*/
private static void tryUncaughtException(final Thread thread,
final Throwable throwable,
final String crashDir,
final Application app) {
// 触发崩溃保险丝
AppCrashSafetyWire.getInstance().burnSafetyWire();
/** 崩溃日志文本(从 CrashHandler 传递过来) */
private String mLog;
// 格式化时间
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss",
Locale.getDefault()).format(new Date());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化崩溃保险丝延迟恢复机制
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
// 创建日志文件
File logParent = TextUtils.isEmpty(crashDir)
? new File(app.getExternalFilesDir(null), "crash")
: new File(crashDir);
final File crashFile = new File(logParent, "crash_" + time + ".txt");
// 获取传递的崩溃日志
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
// 设置系统默认主题(避免自定义主题冲突)
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
// 获取应用版本信息
String versionName = "unknown";
long versionCode = 0;
try {
final PackageInfo packageInfo = app.getPackageManager()
.getPackageInfo(app.getPackageName(), 0);
versionName = packageInfo.versionName;
if (Build.VERSION.SDK_INT >= 28) {
versionCode = packageInfo.getLongVersionCode();
} else {
versionCode = packageInfo.versionCode;
}
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "get package info fail");
}
// 动态创建布局(避免 XML 布局加载异常)
setContentView: {
// 垂直滚动视图(处理日志过长)
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
// 抓取异常堆栈
String fullStackTrace;
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
fullStackTrace = sw.toString();
pw.close();
// 水平滚动视图(处理日志行过长)
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5); // 深色模式灰色背景
// 拼接崩溃头部信息
StringBuilder sb = new StringBuilder();
sb.append("************* Crash Head ****************\n");
sb.append("Time Of Crash : ").append(time).append("\n");
sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n");
sb.append("Device Model : ").append(Build.MODEL).append("\n");
sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n");
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n");
sb.append("App VersionName : ").append(versionName).append("\n");
sb.append("App VersionCode : ").append(versionCode).append("\n");
sb.append("************* Crash Head ****************\n");
sb.append("\n").append(fullStackTrace);
// 日志显示文本框
TextView message = new TextView(this);
{
int padding = dp2px(16); // 内边距 16dp适配不同屏幕
message.setPadding(padding, padding, padding, padding);
message.setText(mLog); // 设置崩溃日志
message.setTextColor(0xFF000000); // 深色模式灰色文字,普通模式黑色文字
message.setTextIsSelectable(true); // 支持文本选择(便于手动复制)
}
final String errorLog = sb.toString();
// 组装布局TextView -> HorizontalScrollView -> ScrollView
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
// 设置当前 Activity 布局
setContentView(contentView);
// 写入日志文件
try {
writeFile(crashFile, errorLog);
} catch (IOException e) {
LogUtils.e(TAG, "write crash log file fail");
}
// 配置 ActionBar 标题和副标题
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
}
}
// 跳转崩溃页面
gotoCrashActivity(errorLog, app);
}
/**
* 重写返回键逻辑:点击返回键直接重启应用
*/
@Override
public void onBackPressed() {
restart();
}
/**
* 写入文本到文件
*/
private static void writeFile(final File file, final String content) throws IOException {
final File parentFile = file.getParentFile();
if (parentFile != null && !parentFile.exists()) {
parentFile.mkdirs();
}
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes());
fos.close();
}
/**
* 重启当前应用(与 GlobalCrashActivity 逻辑一致)
* 清除任务栈,启动主 Activity终止当前进程
*/
private void restart() {
PackageManager pm = getPackageManager();
// 获取应用启动意图(默认启动主 Activity
Intent intent = pm.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
// 设置意图标志:清除原有任务栈,创建新任务
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
);
startActivity(intent);
}
// 关闭当前页面,终止进程,确保完全重启
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* 根据保险丝状态跳转对应崩溃页面
*/
private static void gotoCrashActivity(final String errorLog, final Application app) {
final Intent intent = new Intent();
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
intent.setClass(app, GlobalCrashActivity.class);
} else {
intent.setClass(app, CrashActivity.class);
}
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
/**
* dp 转 px适配不同屏幕密度
* @param dpValue dp 值
* @return 转换后的 px 值
*/
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f); // 四舍五入确保精度
}
try {
if (GlobalApplication.isDebugging()) {
app.startActivity(intent);
} else {
CrashHandleNotifyUtils.handleUncaughtException(app, intent, GlobalCrashActivity.class);
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (ActivityNotFoundException e) {
LogUtils.e(TAG, "CrashActivity not found");
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(Thread.currentThread(), e);
}
} catch (Exception e) {
LogUtils.e(TAG, "start CrashActivity error");
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(Thread.currentThread(), e);
}
}
}
/**
* 菜单点击事件回调(处理复制、重启)
* @param item 被点击的菜单项
* @return false不消费事件保持默认行为
*/
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
// 复制日志到剪贴板
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
// 恢复保险丝到最高等级,然后重启应用
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restart();
break;
}
return false;
}
// ====================== 内部Activity页面 ======================
/**
* 基础极简崩溃页面
* 保险丝熔断时启动,避免复杂布局二次崩溃
*/
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
private static final int MENUITEM_COPY = 0;
private static final int MENUITEM_RESTART = 1;
/**
* 创建 ActionBar 菜单(添加复制、重启项)
* @param menu 菜单容器
* @return true显示菜单
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 添加「复制」菜单:有空间时显示在 ActionBar否则放入溢出菜单
menu.add(0, MENUITEM_COPY, 0, "Copy")
private String mLog;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
initLayout();
}
/**
* 动态初始化布局
*/
private void initLayout() {
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5);
TextView message = new TextView(this);
final int padding = dp2px(16);
message.setPadding(padding, padding, padding, padding);
message.setText(mLog);
message.setTextColor(0xFF000000);
message.setTextIsSelectable(true);
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
}
@Override
public void onBackPressed() {
restartApp();
}
/**
* 重启应用
*/
private void restartApp() {
final Intent intent = getPackageManager()
.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* dp转px
*/
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
// 添加「重启」菜单:同上
menu.add(0, MENUITEM_RESTART, 0, "Restart")
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
}
return true;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
}
}

View File

@@ -10,197 +10,141 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalCrashActivity;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 19:58
* @Describe 应用异常报告观察活动窗口类
* 应用异常报告观察活动窗口类
* 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
* 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025/11/11 19:58:00
* @EditTime 2026/05/11 15:40:12
*/
public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
/** 日志标签(用于调试日志输出,唯一标识当前 Activity */
// ====================== 常量定义 ======================
public static final String TAG = "GlobalCrashActivity";
/** 菜单标识:复制崩溃日志(用于区分菜单项点击事件) */
/** 菜单标识:复制崩溃日志 */
private static final int MENU_ITEM_COPY = 0;
/** 菜单标识:重启应用(用于区分菜单项点击事件) */
/** 菜单标识:重启应用 */
private static final int MENU_ITEM_RESTART = 1;
// ====================== 成员变量 ======================
/** 崩溃报告展示自定义视图 */
// 负责渲染崩溃日志文本、提供 Toolbar 容器,封装了日志展示和菜单样式控制逻辑
private GlobalCrashReportView mCrashReportView;
/** 崩溃日志文本内容 */
// 从 CrashHandler 通过 Intent 传递过来,包含异常堆栈、设备信息等完整崩溃数据
private String mCrashLog;
/**
* Activity 创建时初始化(生命周期核心方法,仅执行一次)
* @param savedInstanceState 保存的实例状态(崩溃页面无需恢复状态,此处仅作兼容)
*/
// ====================== 生命周期方法 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
try {
super.onCreate(savedInstanceState);
protected void onCreate(final Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreate 方法进入");
try {
super.onCreate(savedInstanceState);
final Context appContext = getApplicationContext();
// 初始化崩溃安全防护机制
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(appContext);
// 初始化崩溃安全防护机制
// 作用:防止应用重启后短时间内再次崩溃,由 CrashHandler 内部实现防护逻辑
AppCrashSafetyWire.getInstance()
.postResumeCrashSafetyWireHandler(getApplicationContext());
// 获取传递的崩溃日志
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
LogUtils.d(TAG, "获取到崩溃日志,长度:" + (mCrashLog != null ? mCrashLog.length() : 0));
// 从 Intent 中获取崩溃日志数据EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
setContentView(R.layout.activity_globalcrash);
mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
mCrashReportView.setReport(mCrashLog);
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
setContentView(R.layout.activity_globalcrash);
setActionBar(mCrashReportView.getToolbar());
if (getActionBar() != null) {
getActionBar().setTitle(CrashHandler.TITTLE);
getActionBar().setSubtitle(GlobalApplication.getAppName(appContext));
}
} catch (final Exception e) {
LogUtils.e(TAG, "GlobalCrashActivity onCreate 发生异常", e);
AppCrashSafetyWire.getInstance().burnSafetyWire();
// 初始化崩溃报告展示视图(通过布局 ID 找到自定义 View 实例)
mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
// 将崩溃日志设置到视图中,由自定义 View 负责排版和显示
mCrashReportView.setReport(mCrashLog);
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
final Intent intent = new Intent();
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
// 设置页面的 ActionBar复用自定义 View 中的 Toolbar 作为系统 ActionBar
setActionBar(mCrashReportView.getToolbar());
// 配置 ActionBar 标题和副标题(非空判断避免空指针异常)
if (getActionBar() != null) {
// 设置标题:使用 CrashHandler 中定义的统一标题(如 "应用崩溃报告"
getActionBar().setTitle(CrashHandler.TITTLE);
// 设置副标题:显示当前应用名称(从全局 Application 工具方法获取)
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()));
}
} catch (Exception e) {
// 触发保险丝熔断(每次崩溃熔断一次,降低防护等级)
AppCrashSafetyWire.getInstance().burnSafetyWire();
// 发送一个通知
Intent intent = new Intent();
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
StackTraceElement[] listStackTraceElement = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder("saveConfigData(MainActivity activity)");
for (StackTraceElement item : listStackTraceElement) {
sb.append("\n");
sb.append(item.toString());
}
LogUtils.d(TAG, sb.toString());
finish();
}
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder("GlobalCrashActivity onCreate StackTrace");
for (StackTraceElement item : stackElements) {
sb.append("\n").append(item.toString());
}
LogUtils.d(TAG, sb.toString());
finish();
}
}
/**
* 重写返回键点击事件
* 逻辑:点击手机返回键时,直接重启应用(而非返回上一页,因崩溃后上一页状态可能异常)
*/
@Override
public void onBackPressed() {
LogUtils.d(TAG, "onBackPressed 触发重启应用");
restartApp();
}
/**
* 重启当前应用(核心工具方法)
* 实现逻辑:
* 1. 获取应用的启动意图(默认启动 AndroidManifest 中配置的主 Activity
* 2. 设置意图标志,清除原有任务栈,避免残留异常页面
* 3. 启动主 Activity 并终止当前进程,确保应用完全重启
*/
private void restartApp() {
// 获取 PackageManager 实例(用于获取应用相关信息和意图)
PackageManager packageManager = getPackageManager();
// 获取应用的启动意图(参数为当前应用包名,返回主 Activity 的意图)
Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName());
// ====================== 菜单相关回调 ======================
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
LogUtils.d(TAG, "onCreateOptionsView 初始化菜单");
menu.add(0, MENU_ITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
if (launchIntent != null) {
// 设置意图标志:
// FLAG_ACTIVITY_NEW_TASK创建新的任务栈启动 Activity
// FLAG_ACTIVITY_CLEAR_TOP清除目标 Activity 之上的所有 Activity
// FLAG_ACTIVITY_CLEAR_TASK清除当前任务栈中的所有 Activity
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
// 启动应用主 Activity
startActivity(launchIntent);
}
menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
// 关闭当前崩溃报告页面
finish();
// 终止当前应用进程(确保释放所有资源,避免内存泄漏)
android.os.Process.killProcess(android.os.Process.myPid());
// 强制退出虚拟机(彻底终止应用,防止残留线程继续运行)
System.exit(0);
mCrashReportView.updateMenuStyle();
return true;
}
/**
* 菜单项点击事件回调(实现 MenuItem.OnMenuItemClickListener 接口)
* @param item 被点击的菜单项实例
* @return booleantrue 表示事件已消费不再向下传递false 表示未消费
*/
@Override
public boolean onMenuItemClick(MenuItem item) {
// 根据菜单项 ID 判断点击的是哪个功能
public boolean onMenuItemClick(final MenuItem item) {
LogUtils.d(TAG, "菜单项被点击ID" + item.getItemId());
switch (item.getItemId()) {
case MENU_ITEM_COPY:
// 点击「复制」菜单,执行复制崩溃日志到剪贴板
copyCrashLogToClipboard();
break;
case MENU_ITEM_RESTART:
// 点击「重启」菜单:先恢复崩溃防护机制到最大等级,再重启应用
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
// ====================== 内部私有工具方法 ======================
/**
* 创建页面顶部菜单ActionBar 菜单)
* @param menu 菜单容器,用于添加菜单项
* @return booleantrue 表示显示菜单false 表示不显示
* 重启当前应用
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 添加「复制」菜单项:
// 参数说明:菜单组 ID0 表示默认组)、菜单项 IDMENU_ITEM_COPY、排序号0、菜单文本"Copy"
// setOnMenuItemClickListener(this):绑定点击事件到当前 Activity
// setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM):有空间时显示在 ActionBar 上,否则放入溢出菜单
menu.add(0, MENU_ITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
private void restartApp() {
LogUtils.d(TAG, "开始执行应用重启逻辑");
final PackageManager packageManager = getPackageManager();
final Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName());
// 添加「重启」菜单项(参数含义同上)
menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
// 调用自定义视图的方法,更新菜单文字样式(如颜色、字体大小等,由自定义 View 内部实现)
mCrashReportView.updateMenuStyle();
return true;
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(launchIntent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* 将崩溃日志复制到系统剪贴板(工具方法)
* 功能:用户点击复制菜单后,将完整崩溃日志存入剪贴板,方便粘贴到聊天工具或文档中
* 将崩溃日志复制到系统剪贴板
*/
private void copyCrashLogToClipboard() {
// 获取系统剪贴板服务(需通过 getSystemService 方法获取)
ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// 创建剪贴板数据:
// 参数 1标签用于标识剪贴板内容来源此处用应用包名
// 参数 2实际复制的文本内容崩溃日志
ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog);
// 将数据设置到剪贴板(完成复制操作)
LogUtils.d(TAG, "执行复制崩溃日志到剪贴板");
final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
final ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog);
clipboardManager.setPrimaryClip(clipData);
// 显示复制成功的 Toast 提示(告知用户操作结果)
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
}
}

View File

@@ -8,22 +8,22 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.AppCrashSafetyWire;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.GlobalCrashActivity;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
* 应用崩溃处理通知实用工具集(类库兼容版)
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025/11/29 21:12:00
* @EditTime 2026/05/11 15:38:21
*/
public class CrashHandleNotifyUtils {
// ====================== 常量定义 ======================
public static final String TAG = "CrashHandleNotifyUtils";
/** 通知渠道IDAndroid 8.0+ 必须) */
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
/** 通知渠道名称(用户可见) */
@@ -34,202 +34,186 @@ public class CrashHandleNotifyUtils {
private static final int API_LEVEL_ANDROID_12 = 31;
/** PendingIntent.FLAG_IMMUTABLE 常量值API 31+ */
private static final int FLAG_IMMUTABLE = 0x00000040;
/** 通知内容最大行数控制在3行超出部分省略 */
private static final int NOTIFICATION_MAX_LINES = 3;
// ====================== 对外核心方法 ======================
/**
* 处理未捕获异常(核心方法,类库入口)
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
* 处理未捕获异常(类库入口核心方法
* @param hostApp 宿主Application实例
* @param hostPackageName 宿主应用包名
* @param errorLog 崩溃日志内容
* @param reportCrashActivity 崩溃详情跳转Activity类
*/
public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog, Class<?> reportCrashActivity) {
// 1. 校验核心参数(类库场景必须严格校验,避免空指针)
public static void handleUncaughtException(final Application hostApp,
final String hostPackageName,
final String errorLog,
final Class<?> reportCrashActivity) {
LogUtils.d(TAG, "handleUncaughtException 进入方法");
// 校验入参
if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
LogUtils.e(TAG, "发送崩溃通知失败参数为空hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + "");
LogUtils.e(TAG, "handleUncaughtException 参数为空校验不通过");
return;
}
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
String hostAppName = getHostAppName(hostApp, hostPackageName);
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity
final String hostAppName = getHostAppName(hostApp, hostPackageName);
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog, reportCrashActivity);
}
/**
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
* @param hostApp 宿主应用的 Application 实例
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
* 重载兼容方法:适配原有CrashHandler调用方式
* @param hostApp 宿主Application实例
* @param intent 携带崩溃信息Intent
* @param reportCrashActivity 崩溃详情Activity
*/
public static void handleUncaughtException(Application hostApp, Intent intent, Class<?> reportCrashActivity) {
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
public static void handleUncaughtException(final Application hostApp,
final Intent intent,
final Class<?> reportCrashActivity) {
LogUtils.d(TAG, "handleUncaughtException 重载方法进入");
String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
if (TextUtils.isEmpty(hostPackageName)) {
hostPackageName = hostApp.getPackageName();
LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名" + hostPackageName);
LogUtils.w(TAG, "未携带宿主包名,默认使用应用自身包名");
}
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
// 调用核心方法处理
final String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
handleUncaughtException(hostApp, hostPackageName, errorLog, reportCrashActivity);
}
// ====================== 内部工具方法 ======================
/**
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @return 宿主应用名称(读取失败返回 "未知应用"
* 获取宿主应用名称
* @param hostContext 宿主上下文
* @param hostPackageName 宿主包名
* @return 应用名称失败返回未知应用
*/
private static String getHostAppName(Context hostContext, String hostPackageName) {
private static String getHostAppName(final Context hostContext, final String hostPackageName) {
try {
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
return hostContext.getPackageManager().getApplicationLabel(
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
).toString();
return hostContext.getPackageManager()
.getApplicationLabel(hostContext.getPackageManager()
.getApplicationInfo(hostPackageName, 0)).toString();
} catch (Exception e) {
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + "", e);
LogUtils.e(TAG, "获取宿主应用名称失败", e);
return "未知应用";
}
}
/**
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包
* @param hostAppName 宿主应用的名称(用于通知标题)
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
* 发送崩溃系统通知
* @param hostContext 宿主上下文
* @param hostPackageName 宿主包名
* @param hostAppName 宿主应用名
* @param errorLog 崩溃日志
* @param reportCrashActivity 跳转Activity
*/
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog, Class<?> reportCrashActivity) {
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
private static void sendCrashNotification(final Context hostContext,
final String hostPackageName,
final String hostAppName,
final String errorLog,
final Class<?> reportCrashActivity) {
final NotificationManager notificationManager =
(NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + "");
LogUtils.e(TAG, "获取NotificationManager失败");
return;
}
// 2. 适配 Android 8.0+API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
// 8.0以上创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(hostContext, notificationManager);
}
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog, reportCrashActivity);
final PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext,
hostPackageName, errorLog, reportCrashActivity);
if (jumpIntent == null) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "");
LogUtils.e(TAG, "构建跳转PendingIntent失败");
return;
}
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
final Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
notificationManager.notify(CRASH_NOTIFY_ID, notification);
LogUtils.d(TAG, "崩溃通知发送成功宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
LogUtils.d(TAG, "崩溃通知发送成功宿主包名:" + hostPackageName);
}
/**
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突
* @param hostContext 宿主应用的上下文
* @param notificationManager 宿主的通知管理器
* 创建通知渠道适配Android O及以上
* @param hostContext 宿主上下文
* @param notificationManager 通知管理器
*/
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
// 仅 Android 8.0+ 执行(避免低版本报错)
private static void createCrashNotifyChannel(final Context hostContext,
final NotificationManager notificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 构建通知渠道(归属宿主应用,描述明确类库用途)
android.app.NotificationChannel channel = new android.app.NotificationChannel(
CRASH_NOTIFY_CHANNEL_ID,
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
CRASH_NOTIFY_CHANNEL_ID,
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
);
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
notificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + "渠道ID" + CRASH_NOTIFY_CHANNEL_ID + "");
LogUtils.d(TAG, "通知渠道创建");
}
}
/**
* 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
* @param hostContext 宿主应用的上下文
* @param hostPackageName 宿主应用的包名
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
* @return 跳转崩溃详情页的 PendingIntent
* 构建跳转崩溃详情页PendingIntent
* @param hostContext 宿主上下文
* @param hostPackageName 宿主包名
* @param errorLog 崩溃日志
* @param reportCrashActivity 目标Activity
* @return PendingIntent实例
*/
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog, Class<?> reportCrashActivity) {
private static PendingIntent getGlobalCrashPendingIntent(final Context hostContext,
final String hostPackageName,
final String errorLog,
final Class<?> reportCrashActivity) {
try {
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
Intent crashIntent = new Intent(hostContext, reportCrashActivity);
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity避免类库包名干扰
final Intent crashIntent = new Intent(hostContext, reportCrashActivity);
crashIntent.setPackage(hostPackageName);
// 传递崩溃日志EXTRA_CRASH_INFO与宿主 GlobalCrashActivity 完全匹配)
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 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
hostContext,
CRASH_NOTIFY_ID,
crashIntent,
flags
);
} catch (Exception e) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "", e);
LogUtils.e(TAG, "构建跳转Intent异常", e);
return null;
}
}
/**
* 构建通知实例(类库兼容版)
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
* @param hostContext 宿主应用的上下文
* @param hostAppName 宿主应用的名称(通知标题)
* @param errorLog 崩溃日志(通知内容)
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity
* @return 构建完成的 Notification 对象
* 构建Notification通知实例
* @param hostContext 宿主上下文
* @param hostAppName 宿主应用
* @param errorLog 崩溃日志
* @param jumpIntent 点击跳转意图
* @return 构建好的Notification
*/
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
// 兼容 Android 8.0+指定宿主的通知渠道ID
@SuppressWarnings("deprecation")
private static Notification buildNotification(final Context hostContext,
final String hostAppName,
final String errorLog,
final PendingIntent jumpIntent) {
Notification.Builder builder = new Notification.Builder(hostContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
}
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”使用宿主上下文构建
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
bigTextStyle.bigText(errorLog);
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃");
builder.setStyle(bigTextStyle);
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
builder
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
.setSmallIcon(hostContext.getApplicationInfo().icon)
.setContentTitle(hostAppName + " 崩溃")
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
.setAutoCancel(true) // 点击后自动关闭
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT);
builder.setSmallIcon(hostContext.getApplicationInfo().icon)
.setContentTitle(hostAppName + " 崩溃")
.setContentText(getShortContent(errorLog))
.setContentIntent(jumpIntent)
.setAutoCancel(true)
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT);
// 适配 Android 4.1+:确保在宿主应用中正常显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return builder.build();
} else {
@@ -238,23 +222,24 @@ public class CrashHandleNotifyUtils {
}
/**
* 辅助方法截取日志文本确保显示在3行内通用逻辑无包名依赖
* @param content 完整崩溃日志
* @return 3行内的缩略文
* 截取缩略日志文本
* @param content 原始日志
* @return 缩略文
*/
private static String getShortContent(String content) {
private static String getShortContent(final String content) {
if (content == null || content.isEmpty()) {
return "无崩溃日志";
}
int maxLength = 80; // 估算3行字符数可根据需求调整
final int maxLength = 80;
return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
}
/**
* 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展)
* @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖)
* 资源释放预留方法
* @param hostContext 宿主上下文
*/
public static void release(Context hostContext) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + "");
public static void release(final Context hostContext) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 执行资源释放");
}
}

View File

@@ -12,10 +12,5 @@
android:layout_height="match_parent"
android:id="@+id/activityglobalcrashGlobalCrashReportView1"/>
<Button2
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"/>
</LinearLayout>