diff --git a/appbase/build.properties b/appbase/build.properties index ee1cfc3f..82d9b41d 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 04:34:18 GMT 2025 +#Sun Nov 30 09:00:20 GMT 2025 stageCount=6 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.5 -buildCount=8 +buildCount=9 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 595425d0..b8340443 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 ee1cfc3f..82d9b41d 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 04:34:18 GMT 2025 +#Sun Nov 30 09:00:20 GMT 2025 stageCount=6 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.5 -buildCount=8 +buildCount=9 baseBetaVersion=15.11.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 efd4c278..a839e2bd 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java @@ -186,7 +186,7 @@ public final class CrashHandler { ); try { - if (GlobalApplication.isDebugging()) { + if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) { // 如果是 debug 版,启动崩溃页面窗口 app.startActivity(intent); } else { 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 deleted file mode 100644 index c120808b..00000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java +++ /dev/null @@ -1,330 +0,0 @@ -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 崩溃通知复制活动(最终修复版) - * 核心修复:适配双视图(普通+大视图),确保通知悬浮显示+复制按钮正常显示,支持重复点击 - * 适配场景:类库/独立应用,Android 4.1+ 全版本,兼容各厂商机型 - */ -public class CrashCopyReceiverActivity extends Activity { - - /** 日志 TAG */ - public static final String TAG = "CrashCopyReceiverActivity"; - /** 复制动作 Action(与CrashHandleNotifyUtils完全一致) */ - public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG"; - /** 崩溃日志 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 = "崩溃日志已复制到剪贴板"; - /** 按钮禁用后自动恢复延迟(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); - // 初始化全局Handler(仅创建一次,避免重复创建导致任务混乱) - mMainHandler = new Handler(Looper.getMainLooper()); - // 强制透明无界面(无闪屏,用户无感知) - setTransparentTheme(); - // 初始化宿主信息(包名、应用名,确保类库场景意图正确) - initHostInfo(); - // 处理复制按钮点击逻辑(核心) - handleCopyAction(getIntent()); - } - - /** - * 初始化宿主应用信息(包名、应用名,类库场景关键) - */ - 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, "非复制日志意图,关闭活动"); - finishAndRelease(); - return; - } - - // 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("复制失败:崩溃日志为空"); - finishAndRelease(); - return; - } - - // 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("复制失败,请重试"); - } - - // 6. 关闭透明活动(用户无感知,释放资源) - finishAndRelease(); - } - - /** - * 核心修复:更新通知按钮状态(适配双视图,确保按钮显示+状态切换) - * 调用工具类更新 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 { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // 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(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; - } - } - - /** - * 显示提示(优先使用项目封装的ToastUtils,失败降级系统Toast,确保提示正常显示) - * @param tip 提示内容 - */ - private void showTip(String tip) { - try { - // 优先使用ToastUtils(确保样式统一) - if (ToastUtils.class != null && ToastUtils.isInited()) { - ToastUtils.show(tip); - } else { - // 降级使用系统Toast(避免ToastUtils未初始化导致提示失败) - Toast.makeText(getApplicationContext(), tip, Toast.LENGTH_SHORT).show(); - } - } catch (Exception e) { - LogUtils.e(TAG, "显示提示失败", e); - } - } - - /** - * 统一关闭活动并释放资源(避免内存泄漏,确保资源回收) - */ - 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); - 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 6e9f9fec..00e38425 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,18 +8,21 @@ 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.GlobalCrashActivity; import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.R; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** - * 崩溃通知工具集(优化:2行摘要+按钮防溢出) + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/29 21:12 + * @Describe 应用崩溃处理通知实用工具集(类库兼容版) + * 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志 + * 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用 */ public class CrashHandleNotifyUtils { @@ -35,304 +38,227 @@ public class CrashHandleNotifyUtils { private static final int API_LEVEL_ANDROID_12 = 31; /** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */ private static final int FLAG_IMMUTABLE = 0x00000040; - /** 通知布局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); + + /** 通知内容最大行数(控制在3行,超出部分省略) */ + private static final int NOTIFICATION_MAX_LINES = 3; + /** - * 处理未捕获异常(核心:生成2行摘要) + * 处理未捕获异常(核心方法,类库入口) + * 改进点:新增宿主包名参数,移除类库对固定包名的依赖 + * @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文) + * @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity) + * @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来) */ - public static void handleUncaughtException(Application app, Intent intent) { - String appName = getAppName(app); - String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG); - - if (app == null || appName == null || errorLog == null) { - LogUtils.e(TAG, "发送崩溃通知失败:参数为空"); + public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) { + // 1. 校验核心参数(类库场景必须严格校验,避免空指针) + if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) { + LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")"); return; } - // 核心优化:生成2行摘要(异常类型 + 触发时间) - String crashSummary = getCrash2LineSummary(errorLog); - // 发送通知(传入摘要用于通知栏,完整日志用于大视图) - sendCrashNotification(app, appName, crashSummary, errorLog, app.getPackageName()); + // 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆) + String hostAppName = getHostAppName(hostApp, hostPackageName); + + // 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity) + sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog); } /** - * 核心方法:提取崩溃日志的2行摘要(异常类型 + 触发时间) - * @param errorLog 完整崩溃日志 - * @return 2行字符串(第一行:异常类型;第二行:触发时间) + * 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大) + * 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式 + * @param hostApp 宿主应用的 Application 实例 + * @param intent 存储崩溃信息的意图(extra 中携带崩溃日志) */ - private static String getCrash2LineSummary(String errorLog) { - if (TextUtils.isEmpty(errorLog)) { - return "未知异常\n" + getCurrentTime(); + public static void handleUncaughtException(Application hostApp, Intent intent) { + // 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名) + String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME"); + if (TextUtils.isEmpty(hostPackageName)) { + hostPackageName = hostApp.getPackageName(); + LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName); } - // 第一行:提取异常类型(如: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(); - } - } - } - } + // 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致) + String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG); - // 第二行:当前时间(格式:HH:mm:ss) - String timeStr = getCurrentTime(); - - // 组合为2行摘要(确保仅2行,无多余内容) - return errorType + "\n" + timeStr; + // 调用核心方法处理 + handleUncaughtException(hostApp, hostPackageName, errorLog); } /** - * 获取当前时间(格式:HH:mm:ss) + * 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰) + * @param hostContext 宿主应用的上下文(Application 实例) + * @param hostPackageName 宿主应用的包名 + * @return 宿主应用名称(读取失败返回 "未知应用") */ - private static String getCurrentTime() { - return "触发时间:" + DATE_FORMAT.format(new Date()); - } - - private static String getAppName(Context context) { + private static String getHostAppName(Context hostContext, String hostPackageName) { try { - return context.getPackageManager().getApplicationLabel( - context.getApplicationInfo() + // 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进) + return hostContext.getPackageManager().getApplicationLabel( + hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0) ).toString(); } catch (Exception e) { - LogUtils.e(TAG, "获取应用名称失败", e); + LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e); return "未知应用"; } } /** - * 发送通知(优化:通知栏显示2行摘要,大视图显示完整日志) - * @param context 上下文 - * @param title 通知标题 - * @param crashSummary 2行摘要(通知栏显示) - * @param fullErrorLog 完整日志(大视图显示) - * @param hostPackageName 宿主包名 + * 发送崩溃通知到宿主系统通知栏(类库兼容版) + * 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖 + * @param hostContext 宿主应用的上下文(Application 实例) + * @param hostPackageName 宿主应用的包名 + * @param hostAppName 宿主应用的名称(用于通知标题) + * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) */ - private static void sendCrashNotification(Context context, String title, String crashSummary, - String fullErrorLog, String hostPackageName) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) { + // 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用) + NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) { - LogUtils.e(TAG, "获取NotificationManager失败"); + LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")"); return; } - // 1. 适配Android 8.0+通知渠道(高重要性,支持悬浮) + // 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createCrashNotifyChannel(notificationManager); + createCrashNotifyChannel(hostContext, notificationManager); } - // 2. 构建双视图(普通视图:2行摘要;大视图:完整日志) - RemoteViews remoteViewsNormal = createRemoteViews(context, title, crashSummary, true, NOTIFICATION_LAYOUT_NORMAL); - RemoteViews remoteViewsBig = createRemoteViews(context, title, fullErrorLog, true, NOTIFICATION_LAYOUT_BIG); + // 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity) + PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog); + if (jumpIntent == null) { + LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")"); + return; + } - // 3. 构建意图 - PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); - PendingIntent copyPendingIntent = getCopyPendingIntent(context, fullErrorLog, hostPackageName, true); + // 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主) + Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent); - // 4. 构建通知(优化:确保通知栏按钮完整显示) - Notification.Builder builder = new Notification.Builder(context); + // 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆) + notificationManager.notify(CRASH_NOTIFY_ID, notification); + LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)"); + } + + /** + * 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突) + * @param hostContext 宿主应用的上下文 + * @param notificationManager 宿主的通知管理器 + */ + private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) { + // 仅 Android 8.0+ 执行(避免低版本报错) + 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 + ); + channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)"); + // 注册渠道到宿主的通知管理器,确保渠道归属宿主 + notificationManager.createNotificationChannel(channel); + LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")"); + } + } + + /** + * 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键) + * 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity; + * 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配; + * 3. 使用宿主上下文,避免类库上下文导致的适配问题。 + * @param hostContext 宿主应用的上下文 + * @param hostPackageName 宿主应用的包名 + * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) + * @return 跳转崩溃详情页的 PendingIntent + */ + private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) { + try { + // 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名) + Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class); + // 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰) + 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 + ); + } catch (Exception e) { + LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e); + return null; + } + } + + /** + * 构建通知实例(类库兼容版) + * 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用 + * @param hostContext 宿主应用的上下文 + * @param hostAppName 宿主应用的名称(通知标题) + * @param errorLog 崩溃日志(通知内容) + * @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity) + * @return 构建完成的 Notification 对象 + */ + private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) { + // 兼容 Android 8.0+:指定宿主的通知渠道ID + Notification.Builder builder = new Notification.Builder(hostContext); 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); - } + // 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建) + Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(); + bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); + bigTextStyle.bigText(errorLog); + bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态 + builder.setStyle(bigTextStyle); + // 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主) builder - .setSmallIcon(context.getApplicationInfo().icon) // 必需:小图标 - .setContentIntent(launchPendingIntent) - .setAutoCancel(true) // 点击关闭,不常驻(避免长期占用通知栏) - .setOngoing(false) + // 关键:使用宿主应用的小图标(避免类库图标显示异常) + .setSmallIcon(hostContext.getApplicationInfo().icon) + .setContentTitle(hostAppName + " 崩溃") + .setContentText(getShortContent(errorLog)) // 3行内缩略文本 + .setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity + .setAutoCancel(true) // 点击后自动关闭 .setWhen(System.currentTimeMillis()) - .setTicker("应用崩溃:" + getCrash2LineSummary(fullErrorLog).split("\n")[0]); // 滚动提示:仅显示异常类型 + .setPriority(Notification.PRIORITY_DEFAULT); - // 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, "崩溃通知发送成功(2行摘要+按钮完整显示)"); - } - - /** - * 构建RemoteViews(适配双视图) - */ - 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); + // 适配 Android 4.1+:确保在宿主应用中正常显示 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + return builder.getNotification(); } } /** - * 创建通知渠道(高重要性,支持悬浮) + * 辅助方法:截取日志文本,确保显示在3行内(通用逻辑,无包名依赖) + * @param content 完整崩溃日志 + * @return 3行内的缩略文本 */ - 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 String getShortContent(String content) { + if (content == null || content.isEmpty()) { + return "无崩溃日志"; + } + int maxLength = 80; // 估算3行字符数(可根据需求调整) + return content.length() <= maxLength ? content : content.substring(0, maxLength) + "..."; } /** - * 构建主界面跳转意图 + * 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展) + * @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖) */ - private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) { - Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName); - if (launchIntent == null) { - launchIntent = new Intent(); - } - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { - flags |= FLAG_IMMUTABLE; - } - - return PendingIntent.getActivity( - context, - 0, - launchIntent, - flags - ); - } - - /** - * 构建复制按钮意图 - */ - public static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName, boolean isEnabled) { - Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class); - copyIntent.setPackage(hostPackageName); - copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG); - copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog); - copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_BTN_ENABLED, isEnabled); - copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - - 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; - } - - return PendingIntent.getActivity( - context, - dynamicRequestCode, - copyIntent, - flags - ); - } - - /** - * 关闭通知 - */ - public static void cancelCrashNotification(Context context) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null) { - notificationManager.cancel(CRASH_NOTIFY_ID); - } - } - - public static void release(Context context) { - LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成"); + public static void release(Context hostContext) { + LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")"); } } - diff --git a/libappbase/src/main/res/layout/layout_crash_notification_big.xml b/libappbase/src/main/res/layout/layout_crash_notification_big.xml deleted file mode 100644 index 9b9ab7d7..00000000 --- a/libappbase/src/main/res/layout/layout_crash_notification_big.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - -