diff --git a/appbase/build.properties b/appbase/build.properties index 209a6b9..58b9b6b 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -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 diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/App.java b/appbase/src/main/java/cc/winboll/studio/appbase/App.java index ffc5262..9df2ed8 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -27,7 +27,7 @@ public class App extends GlobalApplication { setIsDebugging(BuildConfig.DEBUG); } // release 版调试码 - setIsDebugging(!BuildConfig.DEBUG); + //setIsDebugging(!BuildConfig.DEBUG); // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) ToastUtils.init(getApplicationContext()); diff --git a/libappbase/build.properties b/libappbase/build.properties index 209a6b9..58b9b6b 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -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 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java index 7bda03d..bfdcf5f 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java @@ -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&豆包大模型 - * @Date 2025/11/11 20:14 - * @Describe * 应用全局崩溃处理类(单例逻辑) + * 应用全局崩溃处理类(单例逻辑) * 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面, * 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用 + * @Author 豆包&ZhanGSKen + * @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; + } + } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java index dd725a6..1b185fd 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java @@ -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&豆包大模型 - * @Date 2025/11/11 19:58 - * @Describe 应用异常报告观察活动窗口类 + * 应用异常报告观察活动窗口类 * 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情, * 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用 + * @Author 豆包&ZhanGSKen + * @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 boolean:true 表示事件已消费,不再向下传递;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 boolean:true 表示显示菜单;false 表示不显示 + * 重启当前应用 */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // 添加「复制」菜单项: - // 参数说明:菜单组 ID(0 表示默认组)、菜单项 ID(MENU_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(); } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java index 535f06a..578e8c0 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java @@ -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&豆包大模型 - * @Date 2025/11/29 21:12 - * @Describe 应用崩溃处理通知实用工具集(类库兼容版) + * 应用崩溃处理通知实用工具集(类库兼容版) * 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志 * 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用 + * @Author 豆包&ZhanGSKen + * @CreateTime 2025/11/29 21:12:00 + * @EditTime 2026/05/11 15:38:21 */ public class CrashHandleNotifyUtils { + // ====================== 常量定义 ====================== public static final String TAG = "CrashHandleNotifyUtils"; - /** 通知渠道ID(Android 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 执行资源释放"); } } + diff --git a/libappbase/src/main/res/layout/activity_globalcrash.xml b/libappbase/src/main/res/layout/activity_globalcrash.xml index b2c53bb..ab9120e 100644 --- a/libappbase/src/main/res/layout/activity_globalcrash.xml +++ b/libappbase/src/main/res/layout/activity_globalcrash.xml @@ -12,10 +12,5 @@ android:layout_height="match_parent" android:id="@+id/activityglobalcrashGlobalCrashReportView1"/> - -