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 资源释放完成(无广播接收器需注销)");
}
}