diff --git a/appbase/build.properties b/appbase/build.properties index 91f0f0f..209a6b9 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon May 11 07:23:43 GMT 2026 +#Mon May 11 08:36:26 GMT 2026 stageCount=6 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.5 -buildCount=4 +buildCount=21 baseBetaVersion=15.20.6 diff --git a/libappbase/build.properties b/libappbase/build.properties index 91f0f0f..209a6b9 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon May 11 07:23:43 GMT 2026 +#Mon May 11 08:36:26 GMT 2026 stageCount=6 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.5 -buildCount=4 +buildCount=21 baseBetaVersion=15.20.6 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/AppCrashSafetyWire.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/AppCrashSafetyWire.java new file mode 100644 index 0000000..9391189 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/AppCrashSafetyWire.java @@ -0,0 +1,194 @@ +package cc.winboll.studio.libappbase; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * 应用崩溃保险丝内部类(单例) + * 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略 + * 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面 + */ +public final class AppCrashSafetyWire { + + public static final String TAG = "AppCrashSafetyWire"; + + /** 单例实例(volatile 保证多线程可见性) */ + private static volatile AppCrashSafetyWire _AppCrashSafetyWire; + + /** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */ + private volatile Integer currentSafetyLevel; + /** 最低熔断等级(1,再崩溃则熔断) */ + private static final int _MINI = 1; + /** 最高熔断等级(2,初始状态) */ + private static final int _MAX = 2; + + /** + * 私有构造方法(单例模式,禁止外部实例化) + * 初始化时加载本地存储的熔断等级 + */ + private AppCrashSafetyWire() { + LogUtils.d(TAG, "AppCrashSafetyWire()"); + currentSafetyLevel = loadCurrentSafetyLevel(); + } + + /** + * 获取单例实例(双重检查锁定,线程安全) + * @return AppCrashSafetyWire 单例 + */ + public static synchronized AppCrashSafetyWire getInstance() { + if (_AppCrashSafetyWire == null) { + _AppCrashSafetyWire = new AppCrashSafetyWire(); + } + return _AppCrashSafetyWire; + } + + /** + * 设置当前熔断等级(内存中) + * @param currentSafetyLevel 目标等级(1~2) + */ + public void setCurrentSafetyLevel(int currentSafetyLevel) { + this.currentSafetyLevel = currentSafetyLevel; + } + + /** + * 获取当前熔断等级(内存中) + * @return 当前等级(1~2 或 null) + */ + public int getCurrentSafetyLevel() { + return currentSafetyLevel; + } + + /** + * 保存熔断等级到本地文件(持久化,重启应用生效) + * @param currentSafetyLevel 待保存的等级 + */ + public void saveCurrentSafetyLevel(int currentSafetyLevel) { + LogUtils.d(TAG, "saveCurrentSafetyLevel()"); + this.currentSafetyLevel = currentSafetyLevel; + try { + // 序列化等级到文件(ObjectOutputStream 写入 int) + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(CrashHandler._CrashCountFilePath)); + oos.writeInt(currentSafetyLevel); + oos.flush(); + oos.close(); + LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel)); + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + + /** + * 从本地文件加载熔断等级(应用启动时初始化) + * @return 加载的等级(文件不存在则初始化为 MAX(2)) + */ + public int loadCurrentSafetyLevel() { + LogUtils.d(TAG, "loadCurrentSafetyLevel()"); + try { + File f = new File(CrashHandler._CrashCountFilePath); + if (f.exists()) { + // 反序列化从文件读取等级 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(CrashHandler._CrashCountFilePath)); + currentSafetyLevel = ois.readInt(); + LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel)); + } else { + // 文件不存在,初始化等级为最高(2)并保存 + currentSafetyLevel = _MAX; + LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX)); + saveCurrentSafetyLevel(currentSafetyLevel); + } + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + return currentSafetyLevel; + } + + /** + * 熔断保险丝(每次崩溃调用,降低防护等级) + * @return 熔断后是否仍在防护范围内(true:是;false:已熔断) + */ + boolean burnSafetyWire() { + LogUtils.d(TAG, "burnSafetyWire()"); + // 加载当前等级 + int safeLevel = loadCurrentSafetyLevel(); + // 若在防护范围内(1~2),等级-1 并保存 + if (isSafetyWireWorking(safeLevel)) { + LogUtils.d(TAG, "burnSafetyWire() use"); + saveCurrentSafetyLevel(safeLevel - 1); + // 返回熔断后的状态 + return isSafetyWireWorking(safeLevel - 1); + } + return false; + } + + /** + * 检查熔断等级是否在有效范围内(1~2) + * @param safetyLevel 待检查的等级 + * @return true:在范围内(防护有效);false:超出范围(已熔断) + */ + boolean isSafetyWireWorking(int safetyLevel) { + LogUtils.d(TAG, "isSafetyWireOK()"); + LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel)); + + if (safetyLevel >= _MINI && safetyLevel <= _MAX) { + LogUtils.d(TAG, String.format("In Safety Level")); + return true; + } + LogUtils.d(TAG, String.format("Out of Safety Level")); + return false; + } + + /** + * 立即恢复熔断等级到最高(2) + * 用于重启应用后重置防护状态 + */ + void resumeToMaximumImmediately() { + LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)"); + AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX); + } + + /** + * 关闭防护(设置等级为最低(1)) + * 下次崩溃直接熔断 + */ + void off() { + LogUtils.d(TAG, "off()"); + saveCurrentSafetyLevel(_MINI); + } + + /** + * 检查当前保险丝是否有效(防护未熔断) + * @return true:有效(等级 1~2);false:已熔断 + */ + public boolean isAppCrashSafetyWireOK() { + LogUtils.d(TAG, "isAppCrashSafetyWireOK()"); + currentSafetyLevel = loadCurrentSafetyLevel(); + return isSafetyWireWorking(currentSafetyLevel); + } + + /** + * 延迟恢复保险丝到最高等级(500ms 后) + * 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃 + * @param context 上下文(用于获取主线程 Handler) + */ + void postResumeCrashSafetyWireHandler(final Context context) { + // 主线程延迟 500ms 执行(避免页面启动时阻塞) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + LogUtils.d(TAG, "Handler run()"); + // 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级 + if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) { + AppCrashSafetyWire.getInstance().resumeToMaximumImmediately(); + LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级"); + } + } + }, 500); + } +} 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 ff04820..7bda03d 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java @@ -164,13 +164,13 @@ public final class CrashHandler { 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,避免复杂页面再次崩溃) @@ -189,11 +189,11 @@ public final class CrashHandler { if (GlobalApplication.isDebugging()) { // 如果是 debug 版,启动崩溃页面窗口 app.startActivity(intent); + } else { + // 发送一个通知 + CrashHandleNotifyUtils.handleUncaughtException(app, intent, GlobalCrashActivity.class); } - // 发送一个通知 - CrashHandleNotifyUtils.handleUncaughtException(app, intent); - // 终止当前进程(确保完全重启) android.os.Process.killProcess(android.os.Process.myPid()); System.exit(0); @@ -236,187 +236,6 @@ public final class CrashHandler { }); } - /** - * 应用崩溃保险丝内部类(单例) - * 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略 - * 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面 - */ - public static final class AppCrashSafetyWire { - - /** 单例实例(volatile 保证多线程可见性) */ - private static volatile AppCrashSafetyWire _AppCrashSafetyWire; - - /** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */ - private volatile Integer currentSafetyLevel; - /** 最低熔断等级(1,再崩溃则熔断) */ - private static final int _MINI = 1; - /** 最高熔断等级(2,初始状态) */ - private static final int _MAX = 2; - - /** - * 私有构造方法(单例模式,禁止外部实例化) - * 初始化时加载本地存储的熔断等级 - */ - private AppCrashSafetyWire() { - LogUtils.d(TAG, "AppCrashSafetyWire()"); - currentSafetyLevel = loadCurrentSafetyLevel(); - } - - /** - * 获取单例实例(双重检查锁定,线程安全) - * @return AppCrashSafetyWire 单例 - */ - public static synchronized AppCrashSafetyWire getInstance() { - if (_AppCrashSafetyWire == null) { - _AppCrashSafetyWire = new AppCrashSafetyWire(); - } - return _AppCrashSafetyWire; - } - - /** - * 设置当前熔断等级(内存中) - * @param currentSafetyLevel 目标等级(1~2) - */ - public void setCurrentSafetyLevel(int currentSafetyLevel) { - this.currentSafetyLevel = currentSafetyLevel; - } - - /** - * 获取当前熔断等级(内存中) - * @return 当前等级(1~2 或 null) - */ - public int getCurrentSafetyLevel() { - return currentSafetyLevel; - } - - /** - * 保存熔断等级到本地文件(持久化,重启应用生效) - * @param currentSafetyLevel 待保存的等级 - */ - public void saveCurrentSafetyLevel(int currentSafetyLevel) { - LogUtils.d(TAG, "saveCurrentSafetyLevel()"); - this.currentSafetyLevel = currentSafetyLevel; - try { - // 序列化等级到文件(ObjectOutputStream 写入 int) - ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath)); - oos.writeInt(currentSafetyLevel); - oos.flush(); - oos.close(); - LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel)); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - } - - /** - * 从本地文件加载熔断等级(应用启动时初始化) - * @return 加载的等级(文件不存在则初始化为 MAX(2)) - */ - public int loadCurrentSafetyLevel() { - LogUtils.d(TAG, "loadCurrentSafetyLevel()"); - try { - File f = new File(_CrashCountFilePath); - if (f.exists()) { - // 反序列化从文件读取等级 - ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath)); - currentSafetyLevel = ois.readInt(); - LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel)); - } else { - // 文件不存在,初始化等级为最高(2)并保存 - currentSafetyLevel = _MAX; - LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX)); - saveCurrentSafetyLevel(currentSafetyLevel); - } - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - return currentSafetyLevel; - } - - /** - * 熔断保险丝(每次崩溃调用,降低防护等级) - * @return 熔断后是否仍在防护范围内(true:是;false:已熔断) - */ - boolean burnSafetyWire() { - LogUtils.d(TAG, "burnSafetyWire()"); - // 加载当前等级 - int safeLevel = loadCurrentSafetyLevel(); - // 若在防护范围内(1~2),等级-1 并保存 - if (isSafetyWireWorking(safeLevel)) { - LogUtils.d(TAG, "burnSafetyWire() use"); - saveCurrentSafetyLevel(safeLevel - 1); - // 返回熔断后的状态 - return isSafetyWireWorking(safeLevel - 1); - } - return false; - } - - /** - * 检查熔断等级是否在有效范围内(1~2) - * @param safetyLevel 待检查的等级 - * @return true:在范围内(防护有效);false:超出范围(已熔断) - */ - boolean isSafetyWireWorking(int safetyLevel) { - LogUtils.d(TAG, "isSafetyWireOK()"); - LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel)); - - if (safetyLevel >= _MINI && safetyLevel <= _MAX) { - LogUtils.d(TAG, String.format("In Safety Level")); - return true; - } - LogUtils.d(TAG, String.format("Out of Safety Level")); - return false; - } - - /** - * 立即恢复熔断等级到最高(2) - * 用于重启应用后重置防护状态 - */ - void resumeToMaximumImmediately() { - LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)"); - AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX); - } - - /** - * 关闭防护(设置等级为最低(1)) - * 下次崩溃直接熔断 - */ - void off() { - LogUtils.d(TAG, "off()"); - saveCurrentSafetyLevel(_MINI); - } - - /** - * 检查当前保险丝是否有效(防护未熔断) - * @return true:有效(等级 1~2);false:已熔断 - */ - public boolean isAppCrashSafetyWireOK() { - LogUtils.d(TAG, "isAppCrashSafetyWireOK()"); - currentSafetyLevel = loadCurrentSafetyLevel(); - return isSafetyWireWorking(currentSafetyLevel); - } - - /** - * 延迟恢复保险丝到最高等级(500ms 后) - * 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃 - * @param context 上下文(用于获取主线程 Handler) - */ - void postResumeCrashSafetyWireHandler(final Context context) { - // 主线程延迟 500ms 执行(避免页面启动时阻塞) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - LogUtils.d(TAG, "Handler run()"); - // 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级 - if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) { - AppCrashSafetyWire.getInstance().resumeToMaximumImmediately(); - LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级"); - } - } - }, 500); - } - } - /** * 基础版崩溃报告页面(保险丝熔断时启动) * 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃 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 9601c99..dd725a6 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java @@ -10,124 +10,197 @@ 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 - * @CreateTime 2025-11-11 19:58:00 - * @EditTime 2025-11-11 20:15:32 + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 19:58 * @Describe 应用异常报告观察活动窗口类 - * 应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情, - * 提供复制日志、重启应用操作入口,便于开发者定位问题及用户恢复应用; - * 页面初始化异常时捕获错误并输出调试日志,提升崩溃页面自身稳定性。 + * 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情, + * 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用 */ -public final class GlobalCrashActivity extends Activity -implements MenuItem.OnMenuItemClickListener { +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) { - super.onCreate(savedInstanceState); - LogUtils.d(TAG, "onCreate: 初始化崩溃展示页面"); + try { + super.onCreate(savedInstanceState); - final Context appContext = getApplicationContext(); - - mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG); - setContentView(R.layout.activity_globalcrash); + // 初始化崩溃安全防护机制 + // 作用:防止应用重启后短时间内再次崩溃,由 CrashHandler 内部实现防护逻辑 + AppCrashSafetyWire.getInstance() + .postResumeCrashSafetyWireHandler(getApplicationContext()); - mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1); - if (mCrashReportView != null) { + // 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键) + mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG); + + // 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构) + setContentView(R.layout.activity_globalcrash); + + // 初始化崩溃报告展示视图(通过布局 ID 找到自定义 View 实例) + mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1); + // 将崩溃日志设置到视图中,由自定义 View 负责排版和显示 mCrashReportView.setReport(mCrashLog); - setActionBar(mCrashReportView.getToolbar()); - } - if (getActionBar() != null) { - getActionBar().setTitle(CrashHandler.TITTLE); - final String appName = GlobalApplication.getAppName(appContext); - getActionBar().setSubtitle(appName); + // 设置页面的 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(); } - LogUtils.d(TAG, "onCreate: 崩溃页面初始化完成"); } + /** + * 重写返回键点击事件 + * 逻辑:点击手机返回键时,直接重启应用(而非返回上一页,因崩溃后上一页状态可能异常) + */ @Override public void onBackPressed() { restartApp(); } /** - * 重启当前应用 + * 重启当前应用(核心工具方法) + * 实现逻辑: + * 1. 获取应用的启动意图(默认启动 AndroidManifest 中配置的主 Activity) + * 2. 设置意图标志,清除原有任务栈,避免残留异常页面 + * 3. 启动主 Activity 并终止当前进程,确保应用完全重启 */ private void restartApp() { - LogUtils.d(TAG, "restartApp: 执行应用重启操作"); - final PackageManager packageManager = getPackageManager(); - final Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName()); + // 获取 PackageManager 实例(用于获取应用相关信息和意图) + PackageManager packageManager = getPackageManager(); + // 获取应用的启动意图(参数为当前应用包名,返回主 Activity 的意图) + Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName()); 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); } + // 关闭当前崩溃报告页面 finish(); + // 终止当前应用进程(确保释放所有资源,避免内存泄漏) android.os.Process.killProcess(android.os.Process.myPid()); + // 强制退出虚拟机(彻底终止应用,防止残留线程继续运行) System.exit(0); } + /** + * 菜单项点击事件回调(实现 MenuItem.OnMenuItemClickListener 接口) + * @param item 被点击的菜单项实例 + * @return boolean:true 表示事件已消费,不再向下传递;false 表示未消费 + */ @Override public boolean onMenuItemClick(MenuItem item) { - final int itemId = item.getItemId(); - switch (itemId) { + // 根据菜单项 ID 判断点击的是哪个功能 + switch (item.getItemId()) { case MENU_ITEM_COPY: + // 点击「复制」菜单,执行复制崩溃日志到剪贴板 copyCrashLogToClipboard(); break; case MENU_ITEM_RESTART: - CrashHandler.AppCrashSafetyWire.getInstance().resumeToMaximumImmediately(); + // 点击「重启」菜单:先恢复崩溃防护机制到最大等级,再重启应用 + 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); + // 添加「重启」菜单项(参数含义同上) menu.add(0, MENU_ITEM_RESTART, 0, "Restart") .setOnMenuItemClickListener(this) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - if (mCrashReportView != null) { - mCrashReportView.updateMenuStyle(); - } + // 调用自定义视图的方法,更新菜单文字样式(如颜色、字体大小等,由自定义 View 内部实现) + mCrashReportView.updateMenuStyle(); + return true; } /** - * 复制崩溃日志到系统剪贴板 + * 将崩溃日志复制到系统剪贴板(工具方法) + * 功能:用户点击复制菜单后,将完整崩溃日志存入剪贴板,方便粘贴到聊天工具或文档中 */ private void copyCrashLogToClipboard() { - LogUtils.d(TAG, "copyCrashLogToClipboard: 复制崩溃日志到剪贴板"); - final ClipboardManager clipboardManager = (ClipboardManager) - getSystemService(Context.CLIPBOARD_SERVICE); - final ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog); + // 获取系统剪贴板服务(需通过 getSystemService 方法获取) + ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + + // 创建剪贴板数据: + // 参数 1:标签(用于标识剪贴板内容来源,此处用应用包名) + // 参数 2:实际复制的文本内容(崩溃日志) + 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 6891ae6..535f06a 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,6 +8,7 @@ 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; @@ -45,7 +46,7 @@ public class CrashHandleNotifyUtils { * @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity) * @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来) */ - public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) { + public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog, Class reportCrashActivity) { // 1. 校验核心参数(类库场景必须严格校验,避免空指针) if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) { LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")"); @@ -56,7 +57,7 @@ public class CrashHandleNotifyUtils { String hostAppName = getHostAppName(hostApp, hostPackageName); // 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity) - sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog); + sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog, reportCrashActivity); } /** @@ -65,7 +66,7 @@ public class CrashHandleNotifyUtils { * @param hostApp 宿主应用的 Application 实例 * @param intent 存储崩溃信息的意图(extra 中携带崩溃日志) */ - public static void handleUncaughtException(Application hostApp, Intent intent) { + public static void handleUncaughtException(Application hostApp, Intent intent, Class reportCrashActivity) { // 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名) String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME"); if (TextUtils.isEmpty(hostPackageName)) { @@ -77,7 +78,7 @@ public class CrashHandleNotifyUtils { String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG); // 调用核心方法处理 - handleUncaughtException(hostApp, hostPackageName, errorLog); + handleUncaughtException(hostApp, hostPackageName, errorLog, reportCrashActivity); } /** @@ -106,7 +107,7 @@ public class CrashHandleNotifyUtils { * @param hostAppName 宿主应用的名称(用于通知标题) * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) */ - private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) { + private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog, Class reportCrashActivity) { // 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用) NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) { @@ -120,7 +121,7 @@ public class CrashHandleNotifyUtils { } // 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity) - PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog); + PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog, reportCrashActivity); if (jumpIntent == null) { LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")"); return; @@ -165,10 +166,10 @@ public class CrashHandleNotifyUtils { * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) * @return 跳转崩溃详情页的 PendingIntent */ - private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) { + private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog, Class reportCrashActivity) { try { // 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名) - Intent crashIntent = new Intent(hostContext, CrashHandler.AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()?GlobalCrashActivity.class : CrashHandler.CrashActivity.class); + Intent crashIntent = new Intent(hostContext, reportCrashActivity); // 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰) crashIntent.setPackage(hostPackageName); // 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配)