From d51d693120993188bd98c0ded6a7de6d9749a7c8 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sun, 30 Nov 2025 12:46:11 +0800 Subject: [PATCH] 20251130_124605_319 --- appbase/build.properties | 4 +- .../java/cc/winboll/studio/appbase/App.java | 4 +- libappbase/build.properties | 4 +- libappbase/src/main/AndroidManifest.xml | 9 +- .../studio/libappbase/CrashHandler.java | 8 +- .../libappbase/GlobalCrashActivity.java | 2 +- .../activities/CrashCopyReceiverActivity.java | 291 ++++++++++--- .../utils/CrashHandleNotifyUtils.java | 390 ++++++++++-------- .../layout/layout_crash_notification_big.xml | 43 ++ .../layout_crash_notification_normal.xml | 50 +++ libappbase/src/main/res/values/colors.xml | 3 + 11 files changed, 568 insertions(+), 240 deletions(-) create mode 100644 libappbase/src/main/res/layout/layout_crash_notification_big.xml create mode 100644 libappbase/src/main/res/layout/layout_crash_notification_normal.xml diff --git a/appbase/build.properties b/appbase/build.properties index 36614d5a..ee1cfc3f 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 11:45:13 HKT 2025 +#Sun Nov 30 04:34:18 GMT 2025 stageCount=6 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.5 -buildCount=0 +buildCount=8 baseBetaVersion=15.11.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 b8340443..595425d0 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -22,8 +22,8 @@ public class App extends GlobalApplication { @Override public void onCreate() { super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) - //setIsDebugging(false); - setIsDebugging(BuildConfig.DEBUG); + setIsDebugging(false); + //setIsDebugging(BuildConfig.DEBUG); // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) ToastUtils.init(getApplicationContext()); } diff --git a/libappbase/build.properties b/libappbase/build.properties index 00c327c3..ee1cfc3f 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 11:43:55 HKT 2025 +#Sun Nov 30 04:34:18 GMT 2025 stageCount=6 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.5 -buildCount=0 +buildCount=8 baseBetaVersion=15.11.6 diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml index e3c479c2..74367522 100644 --- a/libappbase/src/main/AndroidManifest.xml +++ b/libappbase/src/main/AndroidManifest.xml @@ -29,15 +29,16 @@ - + - + android:exported="true" + android:allowTaskReparenting="false" + android:clearTaskOnLaunch="true"> 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 2e9fdf03..efd4c278 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java @@ -53,7 +53,7 @@ public final class CrashHandler { public static final String TITTLE = "CrashReport"; /** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */ - public static final String EXTRA_CRASH_INFO = "crashInfo"; + public static final String EXTRA_CRASH_LOG = "crashInfo"; /** SharedPreferences 存储键(用于记录崩溃状态) */ final static String PREFS = CrashHandler.class.getName() + "PREFS"; @@ -170,12 +170,12 @@ public final class CrashHandler { LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK"); // 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity) intent.setClass(app, GlobalCrashActivity.class); - intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志 + intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志 } else { LogUtils.d(TAG, "gotoCrashActiviy: else"); // 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃) intent.setClass(app, CrashActivity.class); - intent.putExtra(EXTRA_CRASH_INFO, errorLog); + intent.putExtra(EXTRA_CRASH_LOG, errorLog); } // 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面) @@ -436,7 +436,7 @@ public final class CrashHandler { AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext()); // 获取传递的崩溃日志 - mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO); + mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG); // 设置系统默认主题(避免自定义主题冲突) setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar); 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 1d0519d6..2d3f934b 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java @@ -51,7 +51,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe .postResumeCrashSafetyWireHandler(getApplicationContext()); // 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键) - mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO); + mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG); // 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构) setContentView(R.layout.activity_globalcrash); diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java index 08a62b13..c120808b 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java @@ -1,111 +1,260 @@ package cc.winboll.studio.libappbase.activities; import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.view.Window; import android.widget.Toast; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils; /** * @Author ZhanGSKen * @Date 2025/12/01 10:00 - * @Describe 崩溃通知复制动作接收活动(透明无界面) - * 专门接收崩溃通知中“复制日志”按钮的点击事件,处理崩溃日志复制到剪贴板逻辑 - * 优势:相比广播接收器,活动在应用崩溃后仍能被系统唤醒,确保复制功能稳定生效 + * @Describe 崩溃通知复制活动(最终修复版) + * 核心修复:适配双视图(普通+大视图),确保通知悬浮显示+复制按钮正常显示,支持重复点击 + * 适配场景:类库/独立应用,Android 4.1+ 全版本,兼容各厂商机型 */ public class CrashCopyReceiverActivity extends Activity { - /** 日志 TAG(标识当前类日志来源) */ + /** 日志 TAG */ public static final String TAG = "CrashCopyReceiverActivity"; - /** 复制动作 Action(需与 CrashHandleNotifyUtils 中一致) */ + /** 复制动作 Action(与CrashHandleNotifyUtils完全一致) */ public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG"; - /** 崩溃日志 Extra 键(需与 CrashHandleNotifyUtils 中一致) */ + /** 崩溃日志 Extra 键(与CrashHandleNotifyUtils完全一致) */ public static final String EXTRA_CRASH_LOG = "EXTRA_CRASH_LOG"; + /** 按钮状态 Extra 键(传递按钮是否启用) */ + public static final String EXTRA_BTN_ENABLED = "EXTRA_BTN_ENABLED"; /** 复制成功提示文本 */ private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板"; - /** 活动自动关闭延迟(毫秒):避免占用资源,复制完成后快速关闭 */ - private static final long AUTO_FINISH_DELAY = 500; + /** 按钮禁用后自动恢复延迟(1秒,防重复点击) */ + private static final long BTN_ENABLE_DELAY = 1000; + /** Android 12 对应 API 版本号(31) */ + private static final int API_LEVEL_ANDROID_12 = 31; + + // 全局Handler(复用,处理按钮延迟恢复) + private Handler mMainHandler; + // 当前崩溃日志(用于复制+更新通知视图) + private String mCurrentCrashLog; + // 宿主应用名称(用于更新通知标题) + private String mAppName; + // 宿主应用包名(用于构建意图,类库场景必需) + private String mHostPackageName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // 强制设置活动透明(兼容不同主题配置,避免出现白色界面) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - getWindow().setFlags( - android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, - android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - ); - getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT)); - } - - // 处理复制按钮点击事件 + // 初始化全局Handler(仅创建一次,避免重复创建导致任务混乱) + mMainHandler = new Handler(Looper.getMainLooper()); + // 强制透明无界面(无闪屏,用户无感知) + setTransparentTheme(); + // 初始化宿主信息(包名、应用名,确保类库场景意图正确) + initHostInfo(); + // 处理复制按钮点击逻辑(核心) handleCopyAction(getIntent()); } /** - * 处理崩溃日志复制逻辑 - * @param intent 接收的意图(携带崩溃日志) + * 初始化宿主应用信息(包名、应用名,类库场景关键) + */ + private void initHostInfo() { + try { + mHostPackageName = getPackageName(); // 获取宿主应用包名 + mAppName = getPackageManager().getApplicationLabel(getApplicationInfo()).toString(); // 获取宿主应用名称 + } catch (Exception e) { + LogUtils.e(TAG, "初始化宿主信息失败", e); + mHostPackageName = ""; + mAppName = "未知应用"; + } + } + + /** + * 强制设置透明无界面(不依赖Manifest主题,双重保障,避免闪屏) + */ + private void setTransparentTheme() { + // 1. 基础透明配置(API 14+ 兼容) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + getWindow().setFlags( + android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, + android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + ); + // 强制设置背景透明(避免宿主主题覆盖导致白色闪屏) + getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT)); + } + + // 2. 高版本优化(API 21+,去除进入/退出动画,进一步避免闪屏) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setEnterTransition(null); + getWindow().setExitTransition(null); + } + + // 3. 禁用标题栏(避免Manifest配置缺失导致标题栏显示) + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + + /** + * 核心逻辑:处理复制按钮点击(复制日志+更新通知按钮状态) + * 适配双视图更新,确保按钮显示+状态切换正常 + * @param intent 接收的意图(携带崩溃日志+按钮状态) */ private void handleCopyAction(Intent intent) { + // 1. 基础校验(避免非法意图) if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) { - LogUtils.e(TAG, "非崩溃日志复制意图,直接关闭活动"); - finish(); + LogUtils.e(TAG, "非复制日志意图,关闭活动"); + finishAndRelease(); return; } - // 从意图中获取崩溃日志 - String crashLog = intent.getStringExtra(EXTRA_CRASH_LOG); - if (crashLog == null || crashLog.isEmpty()) { + // 2. 获取崩溃日志和按钮状态(从意图中提取,确保数据正确) + mCurrentCrashLog = intent.getStringExtra(EXTRA_CRASH_LOG); + boolean isBtnEnabled = intent.getBooleanExtra(EXTRA_BTN_ENABLED, true); + + // 3. 崩溃日志空校验(避免空指针) + if (mCurrentCrashLog == null || mCurrentCrashLog.isEmpty()) { LogUtils.e(TAG, "崩溃日志为空,无法复制"); showTip("复制失败:崩溃日志为空"); - finish(); + finishAndRelease(); return; } - // 复制日志到剪贴板 - if (copyTextToClipboard(crashLog)) { - LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符"); + // 4. 按钮状态校验(避免禁用时重复点击) + if (!isBtnEnabled) { + LogUtils.w(TAG, "复制按钮已禁用,忽略本次点击"); + finishAndRelease(); + return; + } + + // 5. 执行复制逻辑(适配全版本剪贴板API) + if (copyTextToClipboard(mCurrentCrashLog)) { + LogUtils.d(TAG, "崩溃日志复制成功(长度=" + mCurrentCrashLog.length() + "字符)"); showTip(COPY_SUCCESS_TIP); + // 核心操作1:更新通知按钮为【禁用】状态(仅更新视图,不重复发通知) + updateNotificationBtnState(false); + // 核心操作2:延迟1秒后恢复按钮【启用】状态(支持重复点击) + mMainHandler.postDelayed(new Runnable() { + @Override + public void run() { + updateNotificationBtnState(true); + } + }, BTN_ENABLE_DELAY); } else { LogUtils.e(TAG, "崩溃日志复制失败"); showTip("复制失败,请重试"); } - // 延迟关闭活动(确保提示能正常显示,用户有感知) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - finish(); // 关闭透明活动,释放资源 - // 适配 Android 12+(API 31+):避免活动留在任务栈中(修复 S 常量报错) - if (Build.VERSION.SDK_INT >= 31) { // 核心修复:用 31 替代 Build.VERSION_CODES.S - finishAndRemoveTask(); - } - } - }, AUTO_FINISH_DELAY); + // 6. 关闭透明活动(用户无感知,释放资源) + finishAndRelease(); } /** - * 复制文本到系统剪贴板(兼容全Android版本) - * @param text 需复制的文本(崩溃日志) - * @return true:复制成功;false:复制失败 + * 核心修复:更新通知按钮状态(适配双视图,确保按钮显示+状态切换) + * 调用工具类更新 RemoteViews(普通视图+大视图),不重复发送通知 + * @param isEnabled 按钮是否启用(true:可点击;false:禁用) + */ + private void updateNotificationBtnState(boolean isEnabled) { + try { + // 1. 构建复用意图(主界面跳转意图+复制按钮意图,与工具类逻辑对齐) + PendingIntent launchPendingIntent = getLaunchPendingIntent(); + PendingIntent copyPendingIntent = getCopyPendingIntent(isEnabled); + + // 2. 调用工具类更新按钮状态(同时更新普通视图和大视图,确保悬浮/通知栏按钮都正常) + CrashHandleNotifyUtils.updateNotificationBtnState( + this, + mAppName, + mCurrentCrashLog, + isEnabled, + launchPendingIntent, + copyPendingIntent + ); + } catch (Exception e) { + LogUtils.e(TAG, "更新通知按钮状态失败", e); + } + } + + /** + * 构建主界面跳转意图(复用,用于更新通知时绑定到标题/内容) + */ + private PendingIntent getLaunchPendingIntent() { + // 获取宿主应用主界面意图(确保跳转正确) + Intent launchIntent = getPackageManager().getLaunchIntentForPackage(mHostPackageName); + if (launchIntent == null) { + launchIntent = new Intent(); // 异常处理:避免空意图崩溃 + } + + // 意图标志(适配高版本,确保意图有效) + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { + flags |= 0x00000040; // FLAG_IMMUTABLE 常量值(避免依赖高版本SDK) + } + + return PendingIntent.getActivity( + this, + 0, // 请求码(固定,确保复用) + launchIntent, + flags + ); + } + + /** + * 构建复制按钮意图(根据按钮状态动态生成,适配双视图绑定) + * @param isEnabled 按钮当前是否启用 + */ + private PendingIntent getCopyPendingIntent(boolean isEnabled) { + Intent copyIntent = new Intent(this, CrashCopyReceiverActivity.class); + copyIntent.setPackage(mHostPackageName); // 强制设置宿主包名(类库场景关键) + copyIntent.setAction(ACTION_COPY_CRASH_LOG); // 绑定复制动作 + copyIntent.putExtra(EXTRA_CRASH_LOG, mCurrentCrashLog); // 携带崩溃日志 + copyIntent.putExtra(EXTRA_BTN_ENABLED, isEnabled); // 携带按钮状态 + copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); // 确保活动能唤醒 + + // 动态请求码(避免意图复用锁定,确保每次点击都有效) + int dynamicRequestCode = CrashHandleNotifyUtils.CRASH_NOTIFY_ID + (int) (System.currentTimeMillis() % 1000); + // 意图标志(适配高版本,确保按钮点击有效) + int flags = PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT; + if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { + flags |= 0x00000040; + } + + return PendingIntent.getActivity( + this, + dynamicRequestCode, + copyIntent, + flags + ); + } + + /** + * 复制文本到剪贴板(适配全版本,类库场景容错,避免权限问题) + * @param text 崩溃日志 + * @return true:复制成功;false:失败 */ private boolean copyTextToClipboard(String text) { try { - // 适配 Android 11+(API 30+)剪贴板 API if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + // Android 11+ 剪贴板API(适配高版本) + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard == null) return false; android.content.ClipData clipData = android.content.ClipData.newPlainText("崩溃日志", text); clipboard.setPrimaryClip(clipData); } else { - // 适配 Android 10 及以下版本剪贴板 API - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + // Android 10及以下剪贴板API(适配低版本) + android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard == null) return false; clipboard.setText(text); } return true; + } catch (SecurityException e) { + // 类库场景关键:捕获宿主剪贴板权限异常(部分机型限制类库访问) + LogUtils.e(TAG, "复制失败:宿主剪贴板权限被拒绝", e); + return false; } catch (Exception e) { LogUtils.e(TAG, "复制文本到剪贴板失败", e); return false; @@ -113,17 +262,17 @@ public class CrashCopyReceiverActivity extends Activity { } /** - * 显示提示信息(优先使用 ToastUtils,失败则降级为系统 Toast) + * 显示提示(优先使用项目封装的ToastUtils,失败降级系统Toast,确保提示正常显示) * @param tip 提示内容 */ private void showTip(String tip) { try { - // 优先使用项目封装的 ToastUtils(确保样式统一) - if (ToastUtils.isInited()) { + // 优先使用ToastUtils(确保样式统一) + if (ToastUtils.class != null && ToastUtils.isInited()) { ToastUtils.show(tip); } else { - // 降级使用系统 Toast(确保提示能正常显示) - Toast.makeText(this, tip, Toast.LENGTH_SHORT).show(); + // 降级使用系统Toast(避免ToastUtils未初始化导致提示失败) + Toast.makeText(getApplicationContext(), tip, Toast.LENGTH_SHORT).show(); } } catch (Exception e) { LogUtils.e(TAG, "显示提示失败", e); @@ -131,21 +280,51 @@ public class CrashCopyReceiverActivity extends Activity { } /** - * 处理重复意图(避免多次触发复制) + * 统一关闭活动并释放资源(避免内存泄漏,确保资源回收) + */ + private void finishAndRelease() { + // 关闭透明活动 + finish(); + // Android 12+ 移除任务栈,避免留在最近任务列表 + if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { + finishAndRemoveTask(); + } + // 清空意图,避免重复处理 + setIntent(null); + } + + /** + * 处理重复点击(更新意图并重新执行复制逻辑,确保每次点击都有效) */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - setIntent(intent); // 更新当前意图 - handleCopyAction(intent); // 重新处理复制动作 + if (intent != null && ACTION_COPY_CRASH_LOG.equals(intent.getAction())) { + setIntent(intent); // 更新为最新意图(确保获取最新日志和按钮状态) + handleCopyAction(intent); // 重新执行复制逻辑 + } } /** - * 禁止活动旋转时重建(避免复制逻辑重复执行) + * 禁止活动旋转时重建(避免复制逻辑重复执行,提升稳定性) */ @Override public void onConfigurationChanged(android.content.res.Configuration newConfig) { super.onConfigurationChanged(newConfig); + setIntent(null); // 清空意图,避免旋转后重复处理 + } + + /** + * 活动销毁时释放资源(彻底清理,避免内存泄漏) + */ + @Override + protected void onDestroy() { + super.onDestroy(); + finishAndRelease(); + // 清空Handler所有任务(避免延迟任务导致内存泄漏) + if (mMainHandler != null) { + mMainHandler.removeCallbacksAndMessages(null); + } } } 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 4f68ea2f..6e9f9fec 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 @@ -7,68 +7,109 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.text.TextUtils; +import android.widget.RemoteViews; import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity; import cc.winboll.studio.libappbase.CrashHandler; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.R; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + /** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/29 21:12 - * @Describe 应用崩溃处理通知实用工具集(类库专用,支持宿主应用唤醒活动) - * 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏(3行内容省略+复制按钮),点击复制按钮唤醒CrashCopyReceiverActivity完成日志拷贝 + * 崩溃通知工具集(优化:2行摘要+按钮防溢出) */ public class CrashHandleNotifyUtils { public static final String TAG = "CrashHandleNotifyUtils"; - /** 通知渠道ID(Android 8.0+ 必须,用于归类通知) */ + /** 通知渠道ID(Android 8.0+ 必须) */ private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel"; - /** 通知渠道名称(用户可见,描述渠道用途) */ + /** 通知渠道名称(用户可见) */ private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知"; - /** 通知ID(唯一标识一条通知,避免重复创建) */ - private static final int CRASH_NOTIFY_ID = 0x001; - /** Android 12 对应 API 版本号(31),替代 Build.VERSION_CODES.S */ + /** 通知ID(唯一) */ + public static final int CRASH_NOTIFY_ID = 0x001; + /** Android 12 对应 API 版本号(31) */ private static final int API_LEVEL_ANDROID_12 = 31; - /** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+),避免依赖高版本 SDK */ + /** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */ private static final int FLAG_IMMUTABLE = 0x00000040; - /** 通知内容最大行数(控制在3行,超出部分省略) */ - private static final int NOTIFICATION_MAX_LINES = 3; - /** 复制按钮请求码(区分多个 PendingIntent) */ - private static final int REQUEST_CODE_COPY = 0x002; + /** 通知布局ID(普通+大视图) */ + private static final int NOTIFICATION_LAYOUT_NORMAL = R.layout.layout_crash_notification_normal; // 通知栏(2行摘要) + private static final int NOTIFICATION_LAYOUT_BIG = R.layout.layout_crash_notification_big; // 悬浮/下拉(完整日志) + /** 按钮ID(两个布局一致) */ + public static final int BTN_COPY_ID = R.id.btn_crash_copy; + /** 标题/内容ID(两个布局一致) */ + private static final int TV_TITLE_ID = R.id.tv_crash_title; + private static final int TV_CONTENT_ID = R.id.tv_crash_content; + /** 日期格式化(异常时间显示) */ + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.CHINA); /** - * 处理未捕获异常(核心方法,类库场景专用:强制使用宿主应用包名构建意图) - * 1. 提取应用名称和崩溃日志; - * 2. 创建并发送系统通知(3行内容省略+复制按钮); - * 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。 - * @param app 应用全局 Application 实例(宿主应用的 Application,确保包名正确) - * @param intent 存储崩溃信息的意图(extra 中携带崩溃日志) + * 处理未捕获异常(核心:生成2行摘要) */ public static void handleUncaughtException(Application app, Intent intent) { - // 1. 提取应用名称(优化:从宿主 Application 中获取真实应用名) String appName = getAppName(app); - // 2. 提取崩溃日志(从 Intent Extra 中获取,对应 CrashHandler 存储的崩溃信息) - String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_INFO); + String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG); - // 校验参数(避免空指针,确保通知正常发送) if (app == null || appName == null || errorLog == null) { - LogUtils.e(TAG, "发送崩溃通知失败:参数为空(app=" + app + ", appName=" + appName + ", errorLog=" + errorLog + ")"); + LogUtils.e(TAG, "发送崩溃通知失败:参数为空"); return; } - // 3. 发送崩溃通知到通知栏(类库场景:强制用宿主包名构建意图) - sendCrashNotification(app, appName, errorLog, app.getPackageName()); + // 核心优化:生成2行摘要(异常类型 + 触发时间) + String crashSummary = getCrash2LineSummary(errorLog); + // 发送通知(传入摘要用于通知栏,完整日志用于大视图) + sendCrashNotification(app, appName, crashSummary, errorLog, app.getPackageName()); } /** - * 获取应用真实名称(从宿主 AndroidManifest 中读取 android:label) - * @param context 上下文(宿主 Application 实例,确保获取正确的应用名称) - * @return 应用名称(读取失败返回 "未知应用") + * 核心方法:提取崩溃日志的2行摘要(异常类型 + 触发时间) + * @param errorLog 完整崩溃日志 + * @return 2行字符串(第一行:异常类型;第二行:触发时间) */ + private static String getCrash2LineSummary(String errorLog) { + if (TextUtils.isEmpty(errorLog)) { + return "未知异常\n" + getCurrentTime(); + } + + // 第一行:提取异常类型(如:NullPointerException、IndexOutOfBoundsException) + String errorType = "未知异常"; + if (errorLog.contains("Exception")) { + int startIdx = errorLog.indexOf(':') + 2; // 跳过 "Exception: " 前缀 + int endIdx = errorLog.indexOf('\n'); // 取第一行末尾 + if (startIdx > 0 && endIdx > startIdx) { + errorType = errorLog.substring(startIdx, endIdx).trim(); + } + // 若提取失败,直接取异常类名(如:java.lang.NullPointerException) + if (TextUtils.isEmpty(errorType)) { + String[] lines = errorLog.split("\n"); + if (lines.length > 0) { + String firstLine = lines[0]; + if (firstLine.contains("Exception")) { + errorType = firstLine.substring(firstLine.lastIndexOf('.') + 1).trim(); + } + } + } + } + + // 第二行:当前时间(格式:HH:mm:ss) + String timeStr = getCurrentTime(); + + // 组合为2行摘要(确保仅2行,无多余内容) + return errorType + "\n" + timeStr; + } + + /** + * 获取当前时间(格式:HH:mm:ss) + */ + private static String getCurrentTime() { + return "触发时间:" + DATE_FORMAT.format(new Date()); + } + private static String getAppName(Context context) { try { - // 从宿主包管理器中获取应用信息(类库场景必须用宿主上下文) return context.getPackageManager().getApplicationLabel( context.getApplicationInfo() ).toString(); @@ -79,208 +120,219 @@ public class CrashHandleNotifyUtils { } /** - * 发送崩溃通知到系统通知栏(类库专用:新增宿主包名参数,确保意图跳转正确) - * @param context 上下文(宿主 Application 实例) - * @param title 通知标题(应用名称) - * @param content 通知内容(崩溃日志) - * @param hostPackageName 宿主应用包名(关键:用于构建跨类库的活动意图) + * 发送通知(优化:通知栏显示2行摘要,大视图显示完整日志) + * @param context 上下文 + * @param title 通知标题 + * @param crashSummary 2行摘要(通知栏显示) + * @param fullErrorLog 完整日志(大视图显示) + * @param hostPackageName 宿主包名 */ - private static void sendCrashNotification(Context context, String title, String content, String hostPackageName) { - // 1. 获取通知管理器(系统服务,用于发送/管理通知) + private static void sendCrashNotification(Context context, String title, String crashSummary, + String fullErrorLog, String hostPackageName) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) { - LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空"); + LogUtils.e(TAG, "获取NotificationManager失败"); return; } - // 2. 适配 Android 8.0+(API 26+):创建通知渠道(必须,否则通知不显示) + // 1. 适配Android 8.0+通知渠道(高重要性,支持悬浮) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createCrashNotifyChannel(notificationManager); } - // 3. 构建通知意图(类库场景:用宿主包名构建意图,确保跳转成功) - PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); // 主界面跳转意图 - PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, hostPackageName); // 唤醒复制活动的意图 + // 2. 构建双视图(普通视图:2行摘要;大视图:完整日志) + RemoteViews remoteViewsNormal = createRemoteViews(context, title, crashSummary, true, NOTIFICATION_LAYOUT_NORMAL); + RemoteViews remoteViewsBig = createRemoteViews(context, title, fullErrorLog, true, NOTIFICATION_LAYOUT_BIG); - // 4. 构建通知实例(核心修复:3行内容省略+复制按钮) - Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent); + // 3. 构建意图 + PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); + PendingIntent copyPendingIntent = getCopyPendingIntent(context, fullErrorLog, hostPackageName, true); - // 5. 发送通知(指定通知ID,重复发送同ID会覆盖原通知) + // 4. 构建通知(优化:确保通知栏按钮完整显示) + Notification.Builder builder = new Notification.Builder(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID); + } + + // 双视图设置(普通视图=2行摘要,大视图=完整日志) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + builder.setContent(remoteViewsNormal); // 通知栏:2行摘要 + builder.setCustomBigContentView(remoteViewsBig); // 悬浮/下拉:完整日志 + } + + // 悬浮通知配置(Android 7.0- 高优先级) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder.setPriority(Notification.PRIORITY_HIGH); + builder.setDefaults(Notification.DEFAULT_VIBRATE); + } + + builder + .setSmallIcon(context.getApplicationInfo().icon) // 必需:小图标 + .setContentIntent(launchPendingIntent) + .setAutoCancel(true) // 点击关闭,不常驻(避免长期占用通知栏) + .setOngoing(false) + .setWhen(System.currentTimeMillis()) + .setTicker("应用崩溃:" + getCrash2LineSummary(fullErrorLog).split("\n")[0]); // 滚动提示:仅显示异常类型 + + // 5. 构建并发送通知 + Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + ? builder.build() + : builder.getNotification(); notificationManager.notify(CRASH_NOTIFY_ID, notification); - LogUtils.d(TAG, "崩溃通知发送成功(类库场景):标题=" + title + ",宿主包名=" + hostPackageName); + LogUtils.d(TAG, "崩溃通知发送成功(2行摘要+按钮完整显示)"); } /** - * 创建崩溃通知渠道(Android 8.0+ 必需) - * @param notificationManager 通知管理器 + * 构建RemoteViews(适配双视图) */ - private static void createCrashNotifyChannel(NotificationManager notificationManager) { - // 仅 Android 8.0+ 执行(避免低版本报错) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // 构建通知渠道(指定ID、名称、重要性) - android.app.NotificationChannel channel = new android.app.NotificationChannel( - CRASH_NOTIFY_CHANNEL_ID, - CRASH_NOTIFY_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示) - ); - // 可选:设置渠道描述(用户在设置中可见) - channel.setDescription("用于显示应用崩溃信息,支持复制日志"); - // 注册通知渠道到系统 - notificationManager.createNotificationChannel(channel); - LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID); + private static RemoteViews createRemoteViews(Context context, String title, String content, boolean isBtnEnabled, int layoutId) { + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), layoutId); + + // 设置标题/内容(普通视图=2行摘要,大视图=完整日志) + remoteViews.setTextViewText(TV_TITLE_ID, title + " 崩溃"); // 标题优化:增加"崩溃"标识 + remoteViews.setTextViewText(TV_CONTENT_ID, content); + + // 绑定按钮点击意图 + PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, context.getPackageName(), isBtnEnabled); + remoteViews.setOnClickPendingIntent(BTN_COPY_ID, copyPendingIntent); + + // 按钮状态+颜色 + remoteViews.setBoolean(BTN_COPY_ID, "setEnabled", isBtnEnabled); + int btnColor = isBtnEnabled + ? context.getResources().getColor(R.color.color_btn_enabled) + : context.getResources().getColor(R.color.color_btn_disabled); + remoteViews.setTextColor(BTN_COPY_ID, btnColor); + + // 绑定标题/内容点击意图(跳转主界面) + PendingIntent launchPendingIntent = getLaunchPendingIntent(context, context.getPackageName()); + remoteViews.setOnClickPendingIntent(TV_TITLE_ID, launchPendingIntent); + remoteViews.setOnClickPendingIntent(TV_CONTENT_ID, launchPendingIntent); + + return remoteViews; + } + + /** + * 更新通知按钮状态(同步双视图) + */ + public static void updateNotificationBtnState(Context context, String title, String crashSummary, String fullErrorLog, + boolean isBtnEnabled, PendingIntent contentIntent, + PendingIntent copyPendingIntent) { + try { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + LogUtils.e(TAG, "更新按钮状态失败:NotificationManager为空"); + return; + } + + // 分别更新普通视图(2行摘要)和大视图(完整日志) + RemoteViews remoteViewsNormal = createRemoteViews(context, title + " 崩溃", crashSummary, isBtnEnabled, NOTIFICATION_LAYOUT_NORMAL); + RemoteViews remoteViewsBig = createRemoteViews(context, title + " 崩溃", fullErrorLog, isBtnEnabled, NOTIFICATION_LAYOUT_BIG); + + // 绑定意图 + remoteViewsNormal.setOnClickPendingIntent(BTN_COPY_ID, copyPendingIntent); + remoteViewsBig.setOnClickPendingIntent(BTN_COPY_ID, copyPendingIntent); + remoteViewsNormal.setOnClickPendingIntent(TV_TITLE_ID, contentIntent); + remoteViewsBig.setOnClickPendingIntent(TV_TITLE_ID, contentIntent); + + // 构建通知(仅更新视图) + Notification.Builder builder = new Notification.Builder(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID); + } + + builder + .setSmallIcon(context.getApplicationInfo().icon) + .setContent(remoteViewsNormal) + .setCustomBigContentView(remoteViewsBig) + .setContentIntent(contentIntent) + .setAutoCancel(true) + .setOngoing(false) + .setWhen(System.currentTimeMillis()); + + Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + ? builder.build() + : builder.getNotification(); + notificationManager.notify(CRASH_NOTIFY_ID, notification); + LogUtils.d(TAG, "通知按钮状态更新成功"); + } catch (Exception e) { + LogUtils.e(TAG, "更新通知按钮状态失败", e); } } /** - * 构建通知点击跳转意图(跳转宿主应用主界面,类库场景专用) - * @param context 上下文(宿主 Application 实例) - * @param hostPackageName 宿主应用包名(关键:确保跳转宿主主界面) - * @return 主界面跳转 PendingIntent + * 创建通知渠道(高重要性,支持悬浮) + */ + private static void createCrashNotifyChannel(NotificationManager notificationManager) { + android.app.NotificationChannel channel = new android.app.NotificationChannel( + CRASH_NOTIFY_CHANNEL_ID, + CRASH_NOTIFY_CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("应用崩溃通知(显示2行摘要,支持复制完整日志)"); + channel.enableVibration(true); + channel.setVibrationPattern(new long[]{100, 200}); + notificationManager.createNotificationChannel(channel); + } + + /** + * 构建主界面跳转意图 */ private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) { - // 1. 获取宿主应用主界面 Intent(强制用宿主包名获取,避免类库包名干扰) Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName); if (launchIntent == null) { - // 异常处理:若主界面 Intent 为空,创建空意图(避免崩溃) launchIntent = new Intent(); } - // 2. 构建 PendingIntent(延迟执行的意图,类库场景增加 FLAG_ONE_SHOT 确保单次有效) int flags = PendingIntent.FLAG_UPDATE_CURRENT; - // 适配 Android 12+(API 31+):添加 FLAG_IMMUTABLE 避免安全警告 if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { flags |= FLAG_IMMUTABLE; } return PendingIntent.getActivity( context, - 0, // 请求码(可忽略) + 0, launchIntent, flags ); } /** - * 构建复制按钮意图(类库场景专用:用宿主包名唤醒活动,确保跳转成功) - * @param context 上下文(宿主 Application 实例) - * @param errorLog 崩溃日志(需要复制的内容) - * @param hostPackageName 宿主应用包名(关键:确保系统能找到类库中的活动) - * @return 唤醒复制活动的 PendingIntent + * 构建复制按钮意图 */ - private static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName) { - // 1. 构建唤醒 CrashCopyReceiverActivity 的显式意图(类库场景关键:指定宿主包名) + public static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName, boolean isEnabled) { Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class); - // 强制设置意图的包名为宿主包名(解决类库与宿主包名不匹配问题) copyIntent.setPackage(hostPackageName); - // 设置动作(与Activity中匹配,确保意图精准匹配) copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG); - // 携带完整崩溃日志(键与Activity中一致) copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog); - // 类库场景增强标志:确保活动在宿主应用中能被唤醒,且不重复创建 - copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_SINGLE_TOP - | Intent.FLAG_ACTIVITY_NO_HISTORY); // 不保留活动历史,避免残留 + copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_BTN_ENABLED, isEnabled); + copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - // 2. 构建 PendingIntent(类库场景:使用 FLAG_ONE_SHOT 避免重复触发) - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; - // 适配 Android 12+(API 31+):添加 FLAG_IMMUTABLE 避免安全警告 + int dynamicRequestCode = CRASH_NOTIFY_ID + (int) (System.currentTimeMillis() % 1000); + int flags = PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT; if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { flags |= FLAG_IMMUTABLE; } - // 3. 返回活动类型的 PendingIntent(类库场景:用宿主上下文构建,确保权限通过) return PendingIntent.getActivity( context, - REQUEST_CODE_COPY, // 唯一请求码,区分主界面意图 + dynamicRequestCode, copyIntent, flags ); } /** - * 构建通知实例(核心修复:3行内容省略+复制按钮) - * @param context 上下文(宿主 Application 实例) - * @param title 通知标题(应用名称) - * @param content 通知内容(崩溃日志) - * @param launchPendingIntent 通知点击跳转意图 - * @param copyPendingIntent 唤醒复制活动的意图 - * @return 构建完成的 Notification 对象 + * 关闭通知 */ - private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent, PendingIntent copyPendingIntent) { - // 兼容 Android 8.0+:指定通知渠道ID - Notification.Builder builder = new Notification.Builder(context); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID); - } - - // 核心修复1:用BigTextStyle控制“默认3行省略,下拉显示完整” - Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(); - bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); // 底部省略提示 - bigTextStyle.bigText(content); // 完整日志(下拉时显示) - bigTextStyle.setBigContentTitle(title); // 下拉后的标题(与主标题一致) - builder.setStyle(bigTextStyle); - - // 核心修改2:添加复制按钮(Android 4.1+ 支持通知按钮,类库场景用系统图标避免资源缺失) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // 类库场景:用系统默认复制图标(避免自定义图标在宿主应用中缺失) - int copyIcon = R.drawable.ic_content_copy; - // 若类库有自定义图标,优先使用(需确保宿主应用能访问类库资源) - if (R.drawable.ic_content_copy != 0) { - copyIcon = R.drawable.ic_content_copy; - } - builder.addAction( - copyIcon, // 兼容图标(系统图标兜底) - "复制日志", // 按钮文本 - copyPendingIntent // 按钮点击意图(唤醒CrashCopyReceiverActivity) - ); - } - - // 配置通知核心参数(移除setLines,避免报错) - builder - .setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(用宿主应用图标,避免类库图标缺失) - .setContentTitle(title) // 通知主标题(应用名称) - .setContentText(getShortContent(content)) // 核心:3行内缩略文本 - .setContentIntent(launchPendingIntent) // 通知主体点击跳转主界面 - .setAutoCancel(true) // 点击通知后自动取消 - .setWhen(System.currentTimeMillis()) // 通知创建时间 - .setPriority(Notification.PRIORITY_DEFAULT); // 通知优先级 - - // 适配 Android 4.1+:确保文本显示正常,构建并返回通知 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return builder.build(); - } else { - // Android 4.0 及以下版本,使用 getNotification() 方法 - return builder.getNotification(); + public static void cancelCrashNotification(Context context) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.cancel(CRASH_NOTIFY_ID); } } - /** - * 辅助方法:截取日志文本,确保显示在3行内(按字符数估算,适配大多数设备) - * @param content 完整崩溃日志 - * @return 3行内的缩略文本 - */ - private static String getShortContent(String content) { - if (content == null || content.isEmpty()) { - return "无崩溃日志"; - } - // 估算3行字符数(80字符,可根据设备屏幕调整,避免因字符过长导致换行超3行) - int maxLength = 80; - if (content.length() <= maxLength) { - return content; // 不足3行,直接返回完整文本 - } else { - // 超出3行,截取前80字符并加省略号(确保视觉上仅显示3行) - return content.substring(0, maxLength) + "..."; - } - } - - /** - * 释放资源(删除原广播注销逻辑,仅保留空实现便于兼容旧代码调用) - * @param context 上下文(宿主 Application 实例) - */ public static void release(Context context) { - // 因已移除广播接收器,此处仅保留空实现,避免调用方报错 - LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(类库场景,无广播接收器需注销)"); + LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成"); } } diff --git a/libappbase/src/main/res/layout/layout_crash_notification_big.xml b/libappbase/src/main/res/layout/layout_crash_notification_big.xml new file mode 100644 index 00000000..9b9ab7d7 --- /dev/null +++ b/libappbase/src/main/res/layout/layout_crash_notification_big.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + +