From be6b7841ed1290dcd823191bfda1ad04b4f747a1 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sun, 30 Nov 2025 03:40:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E4=B8=80=E4=B8=AA=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E7=B1=BB=E6=8E=A5=E6=94=B6=E5=B4=A9=E6=BA=83=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E7=9A=84=E5=A4=8D=E5=88=B6=E6=8C=89=E9=92=AE=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=A4=8D=E5=88=B6=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E6=97=A5=E5=BF=97=E5=88=B0=E5=89=AA=E8=B4=B4=E6=9D=BF?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- appbase/src/main/AndroidManifest.xml | 12 +- .../java/cc/winboll/studio/appbase/App.java | 2 +- libappbase/build.properties | 4 +- libappbase/src/main/AndroidManifest.xml | 17 +- .../activities/CrashCopyReceiverActivity.java | 151 +++++++++++++++ .../utils/CrashHandleNotifyUtils.java | 174 ++++-------------- 7 files changed, 216 insertions(+), 148 deletions(-) create mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java diff --git a/appbase/build.properties b/appbase/build.properties index 96256333..dcb2b8c7 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Nov 29 21:41:11 HKT 2025 +#Sat Nov 29 19:40:09 GMT 2025 stageCount=4 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.3 -buildCount=0 +buildCount=3 baseBetaVersion=15.11.4 diff --git a/appbase/src/main/AndroidManifest.xml b/appbase/src/main/AndroidManifest.xml index 284d7afd..2b21bf83 100644 --- a/appbase/src/main/AndroidManifest.xml +++ b/appbase/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + - - + - + + + - + \ No newline at end of file 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 76615aa8..62daf512 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -22,7 +22,7 @@ public class App extends GlobalApplication { @Override public void onCreate() { super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) - setIsDebugging(!BuildConfig.DEBUG); + setIsDebugging(BuildConfig.DEBUG); // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) ToastUtils.init(getApplicationContext()); } diff --git a/libappbase/build.properties b/libappbase/build.properties index 96256333..dcb2b8c7 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Nov 29 21:41:11 HKT 2025 +#Sat Nov 29 19:40:09 GMT 2025 stageCount=4 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.3 -buildCount=0 +buildCount=3 baseBetaVersion=15.11.4 diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml index 28162c61..e3c479c2 100644 --- a/libappbase/src/main/AndroidManifest.xml +++ b/libappbase/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ package="cc.winboll.studio.libappbase"> - + + + + + + + + + + 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 new file mode 100644 index 00000000..08a62b13 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/CrashCopyReceiverActivity.java @@ -0,0 +1,151 @@ +package cc.winboll.studio.libappbase.activities; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +/** + * @Author ZhanGSKen + * @Date 2025/12/01 10:00 + * @Describe 崩溃通知复制动作接收活动(透明无界面) + * 专门接收崩溃通知中“复制日志”按钮的点击事件,处理崩溃日志复制到剪贴板逻辑 + * 优势:相比广播接收器,活动在应用崩溃后仍能被系统唤醒,确保复制功能稳定生效 + */ +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"; + /** 复制成功提示文本 */ + private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板"; + /** 活动自动关闭延迟(毫秒):避免占用资源,复制完成后快速关闭 */ + private static final long AUTO_FINISH_DELAY = 500; + + @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)); + } + + // 处理复制按钮点击事件 + handleCopyAction(getIntent()); + } + + /** + * 处理崩溃日志复制逻辑 + * @param intent 接收的意图(携带崩溃日志) + */ + private void handleCopyAction(Intent intent) { + if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) { + LogUtils.e(TAG, "非崩溃日志复制意图,直接关闭活动"); + finish(); + return; + } + + // 从意图中获取崩溃日志 + String crashLog = intent.getStringExtra(EXTRA_CRASH_LOG); + if (crashLog == null || crashLog.isEmpty()) { + LogUtils.e(TAG, "崩溃日志为空,无法复制"); + showTip("复制失败:崩溃日志为空"); + finish(); + return; + } + + // 复制日志到剪贴板 + if (copyTextToClipboard(crashLog)) { + LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符"); + showTip(COPY_SUCCESS_TIP); + } 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); + } + + /** + * 复制文本到系统剪贴板(兼容全Android版本) + * @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.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); + clipboard.setText(text); + } + return true; + } catch (Exception e) { + LogUtils.e(TAG, "复制文本到剪贴板失败", e); + return false; + } + } + + /** + * 显示提示信息(优先使用 ToastUtils,失败则降级为系统 Toast) + * @param tip 提示内容 + */ + private void showTip(String tip) { + try { + // 优先使用项目封装的 ToastUtils(确保样式统一) + if (ToastUtils.isInited()) { + ToastUtils.show(tip); + } else { + // 降级使用系统 Toast(确保提示能正常显示) + Toast.makeText(this, tip, Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + LogUtils.e(TAG, "显示提示失败", e); + } + } + + /** + * 处理重复意图(避免多次触发复制) + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); // 更新当前意图 + handleCopyAction(intent); // 重新处理复制动作 + } + + /** + * 禁止活动旋转时重建(避免复制逻辑重复执行) + */ + @Override + public void onConfigurationChanged(android.content.res.Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } +} + 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 ea52a5b5..edf4c04d 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 @@ -6,8 +6,8 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.os.Build; +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; @@ -16,7 +16,7 @@ import cc.winboll.studio.libappbase.R; * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/29 21:12 * @Describe 应用崩溃处理通知实用工具集 - * 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏(3行内容省略+复制按钮),点击复制按钮可将完整崩溃日志复制到剪贴板 + * 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏(3行内容省略+复制按钮),点击复制按钮唤醒CrashCopyReceiverActivity完成日志拷贝 */ public class CrashHandleNotifyUtils { @@ -34,15 +34,8 @@ public class CrashHandleNotifyUtils { private static final int FLAG_IMMUTABLE = 0x00000040; /** 通知内容最大行数(控制在3行,超出部分省略) */ private static final int NOTIFICATION_MAX_LINES = 3; - /** 复制按钮 Action(用于区分通知按钮点击事件) */ - private static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG"; /** 复制按钮请求码(区分多个 PendingIntent) */ private static final int REQUEST_CODE_COPY = 0x002; - /** 复制成功提示文本 */ - private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板"; - - // 静态广播接收器(避免重复注册,确保崩溃后仍能接收点击事件) - private static CopyCrashLogReceiver sCopyReceiver; /** * 处理未捕获异常(核心方法) @@ -64,10 +57,7 @@ public class CrashHandleNotifyUtils { return; } - // 3. 注册静态广播接收器(仅注册一次,确保崩溃后能接收点击事件) - registerCopyReceiver(app); - - // 4. 发送崩溃通知到通知栏(3行省略+复制按钮) + // 3. 发送崩溃通知到通知栏(3行省略+复制按钮,点击唤醒CrashCopyReceiverActivity) sendCrashNotification(app, appName, errorLog); } @@ -90,7 +80,7 @@ public class CrashHandleNotifyUtils { } /** - * 发送崩溃通知到系统通知栏(核心修改:3行内容+复制按钮) + * 发送崩溃通知到系统通知栏(核心修改:替换为活动唤醒方案) * @param context 上下文(Application 实例,确保后台也能发送) * @param title 通知标题(应用名称) * @param content 通知内容(崩溃日志) @@ -108,11 +98,11 @@ public class CrashHandleNotifyUtils { createCrashNotifyChannel(notificationManager); } - // 3. 构建通知意图(点击通知跳转主界面 + 点击复制按钮复制日志) + // 3. 构建通知意图(点击通知跳转主界面 + 点击复制按钮唤醒复制活动) PendingIntent launchPendingIntent = getLaunchPendingIntent(context); // 主界面跳转意图 - PendingIntent copyPendingIntent = getCopyPendingIntent(context, content); // 复制日志意图 + PendingIntent copyPendingIntent = getCopyPendingIntent(context, content); // 唤醒复制活动的意图 - // 4. 构建通知实例(核心修复:3行内容省略+复制按钮,修复setLines报错) + // 4. 构建通知实例(核心修复:3行内容省略+复制按钮) Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent); // 5. 发送通知(指定通知ID,重复发送同ID会覆盖原通知) @@ -173,24 +163,31 @@ public class CrashHandleNotifyUtils { } /** - * 构建复制按钮意图(点击复制崩溃日志到剪贴板) + * 构建复制按钮意图(核心修改:点击唤醒 CrashCopyReceiverActivity 完成日志拷贝) + * 替代原广播意图,确保应用崩溃后仍能稳定触发复制 * @param context 上下文 * @param errorLog 崩溃日志(需要复制的内容) - * @return 复制日志 PendingIntent + * @return 唤醒复制活动的 PendingIntent */ private static PendingIntent getCopyPendingIntent(Context context, String errorLog) { - // 1. 构建复制日志的隐式意图(指定 Action,用于 BroadcastReceiver 接收) - Intent copyIntent = new Intent(ACTION_COPY_CRASH_LOG); - copyIntent.putExtra("EXTRA_CRASH_LOG", errorLog); // 携带崩溃日志 - copyIntent.setPackage(context.getPackageName()); // 限制仅当前应用接收,避免安全问题 + // 1. 构建唤醒 CrashCopyReceiverActivity 的显式意图(指定目标活动,避免意图匹配失败) + Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class); + // 设置动作(与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); - // 2. 构建 PendingIntent(使用广播类型,崩溃后仍能触发) + // 2. 构建 PendingIntent(活动类型,优先级高于广播) 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.getBroadcast( + // 3. 返回活动类型的 PendingIntent(替代原广播类型) + return PendingIntent.getActivity( context, REQUEST_CODE_COPY, // 唯一请求码,区分主界面意图 copyIntent, @@ -199,100 +196,12 @@ public class CrashHandleNotifyUtils { } /** - * 注册静态广播接收器(仅注册一次,确保崩溃后能接收点击事件) - * 解决动态广播崩溃后被销毁的问题 - * @param context 上下文(Application 实例) - */ - private static void registerCopyReceiver(Context context) { - // 避免重复注册(静态接收器仅注册一次) - if (sCopyReceiver != null) { - return; - } - - // 构建广播过滤器(仅接收复制日志的 Action) - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_COPY_CRASH_LOG); - filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - - // 初始化静态广播接收器 - sCopyReceiver = new CopyCrashLogReceiver(); - // 注册广播(使用 Application 上下文,确保生命周期与应用一致) - context.registerReceiver(sCopyReceiver, filter); - LogUtils.d(TAG, "复制日志广播接收器注册成功"); - } - - /** - * 静态广播接收器(处理复制按钮点击事件) - * 静态内部类避免内存泄漏,且崩溃后仍能接收系统发送的广播 - */ - private static class CopyCrashLogReceiver extends android.content.BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // 验证 Action,确保是复制日志的点击事件 - if (ACTION_COPY_CRASH_LOG.equals(intent.getAction())) { - // 从意图中获取完整崩溃日志 - String crashLog = intent.getStringExtra("EXTRA_CRASH_LOG"); - if (crashLog != null && !crashLog.isEmpty()) { - // 复制日志到剪贴板 - copyTextToClipboard(context, "崩溃日志", crashLog); - // 复制成功后显示提示(可选,提升用户体验) - showCopySuccessTip(context); - LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符"); - } else { - LogUtils.e(TAG, "复制崩溃日志失败:日志为空"); - } - } - } - } - - /** - * 复制文本到系统剪贴板(修复适配逻辑,确保全版本可用) - * @param context 上下文 - * @param label 剪贴板文本标签(用户不可见,用于区分剪贴板内容) - * @param text 需要复制的文本(崩溃日志) - */ - private static void copyTextToClipboard(Context context, String label, String text) { - try { - // 适配 Android 11+(API 30+)剪贴板 API,兼容低版本 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clipData = android.content.ClipData.newPlainText(label, text); - clipboard.setPrimaryClip(clipData); - } else { - // 低版本剪贴板 API(Android 10 及以下) - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(text); - } - } catch (Exception e) { - LogUtils.e(TAG, "复制文本到剪贴板失败", e); - } - } - - /** - * 显示复制成功提示(Toast,提升用户体验) - * @param context 上下文 - */ - private static void showCopySuccessTip(Context context) { - try { - // 若项目中 ToastUtils 支持后台显示,用 ToastUtils;否则用系统 Toast - if (cc.winboll.studio.libappbase.ToastUtils.isInited()) { - cc.winboll.studio.libappbase.ToastUtils.show(COPY_SUCCESS_TIP); - } else { - // 系统 Toast 适配(确保后台能显示) - android.widget.Toast.makeText(context, COPY_SUCCESS_TIP, android.widget.Toast.LENGTH_SHORT).show(); - } - } catch (Exception e) { - LogUtils.e(TAG, "显示复制成功提示失败", e); - } - } - - /** - * 构建通知实例(核心修复:3行内容省略+复制按钮,修复setLines报错) + * 构建通知实例(核心修复:3行内容省略+复制按钮) * @param context 上下文 * @param title 通知标题(应用名称) * @param content 通知内容(崩溃日志) * @param launchPendingIntent 通知点击跳转意图 - * @param copyPendingIntent 复制按钮点击意图 + * @param copyPendingIntent 唤醒复制活动的意图 * @return 构建完成的 Notification 对象 */ private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent, PendingIntent copyPendingIntent) { @@ -311,11 +220,11 @@ public class CrashHandleNotifyUtils { // 核心修改2:添加复制按钮(Android 4.1+ 支持通知按钮) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // 复制按钮:自定义图标+文本+点击意图(确保图标存在) + // 复制按钮:自定义图标+文本+点击意图(绑定唤醒复制活动) builder.addAction( - R.drawable.ic_content_copy, // 自定义复制图标(需确保drawable目录下存在,否则替换为系统图标) + R.drawable.ic_content_copy, // 自定义复制图标(需确保drawable目录下存在) "复制日志", // 按钮文本 - copyPendingIntent // 按钮点击意图(绑定复制广播) + copyPendingIntent // 按钮点击意图(唤醒CrashCopyReceiverActivity) ); } @@ -329,14 +238,13 @@ public class CrashHandleNotifyUtils { .setWhen(System.currentTimeMillis()) // 通知创建时间 .setPriority(Notification.PRIORITY_DEFAULT); // 通知优先级 - // 适配 Android 4.1+:确保文本显示正常 - // 构建通知并返回(兼容低版本,区分 API 等级避免报错) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return builder.build(); - } else { - // Android 4.0 及以下版本,使用 getNotification() 方法 - return builder.getNotification(); - } + // 适配 Android 4.1+:确保文本显示正常,构建并返回通知 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + // Android 4.0 及以下版本,使用 getNotification() 方法 + return builder.getNotification(); + } } /** @@ -360,20 +268,12 @@ public class CrashHandleNotifyUtils { } /** - * 释放资源(可选,在 Application 销毁时调用,避免内存泄漏) + * 释放资源(删除原广播注销逻辑,仅保留空实现便于兼容旧代码调用) * @param context 上下文(Application 实例) */ public static void release(Context context) { - // 注销静态广播接收器,避免内存泄漏 - if (sCopyReceiver != null && context != null) { - try { - context.unregisterReceiver(sCopyReceiver); - sCopyReceiver = null; - LogUtils.d(TAG, "复制日志广播接收器已注销"); - } catch (Exception e) { - LogUtils.e(TAG, "注销广播接收器失败", e); - } - } + // 因已移除广播接收器,此处仅保留空实现,避免调用方报错 + LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(无广播接收器需注销)"); } }