Compare commits

..

22 Commits

Author SHA1 Message Date
0bb70fae6e <libappbase>Library Release 15.11.6 2025-11-30 17:13:09 +08:00
4b11f27d09 <appbase>APK 15.11.6 release Publish. 2025-11-30 17:12:49 +08:00
12dc8eef6b <powerbell>Start New Stage Version. 2025-11-30 17:06:46 +08:00
8930c43bcd <powerbell>Start New Stage Version. 2025-11-30 17:06:28 +08:00
a138635cae 回退到应用崩溃通知悬浮窗可用版本,并且修改通知跳转窗口。 2025-11-30 17:04:24 +08:00
ad6d29c27e 改进崩溃通知的各类源码作为类库适配能力。 2025-11-30 16:29:37 +08:00
8aafcabba9 20251130_162041_979 2025-11-30 16:21:03 +08:00
fc34ed0d5a 20251130_161248_108 2025-11-30 16:12:56 +08:00
d51d693120 20251130_124605_319 2025-11-30 12:46:11 +08:00
350118301d <libappbase>Library Release 15.11.5 2025-11-30 11:45:17 +08:00
e47a64cc87 <appbase>APK 15.11.5 release Publish. 2025-11-30 11:43:55 +08:00
65161b1a80 改进应用崩溃处理作为类库使用的鲁棒性 2025-11-30 11:42:18 +08:00
aa24bc5e11 <libappbase>Library Release 15.11.4 2025-11-30 03:45:10 +08:00
1b24fc99ef <appbase>APK 15.11.4 release Publish. 2025-11-30 03:42:05 +08:00
be6b7841ed 定义一个活动类接收崩溃通知的复制按钮动作,用于复制崩溃日志到剪贴板。 2025-11-30 03:40:48 +08:00
e4dc8109aa 编译参数修复 2025-11-30 01:51:05 +08:00
d0e818056a 编译参数修复 2025-11-30 01:50:04 +08:00
c744289896 20251130_014538_531 2025-11-30 01:45:43 +08:00
375c5c1168 <appbase>APK 15.11.3 release Publish. 2025-11-29 21:41:11 +08:00
5d2d397113 调试标志位设置 2025-11-29 21:39:52 +08:00
fdba61f30c 添加发布版应用异常处理方案 2025-11-29 21:38:23 +08:00
d87172a60d <libappbase>Library Release 15.11.2 2025-11-29 02:41:26 +08:00
11 changed files with 339 additions and 22 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Nov 29 02:40:35 HKT 2025
stageCount=3
#Sun Nov 30 17:13:07 HKT 2025
stageCount=7
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.2
publishVersion=15.11.6
buildCount=0
baseBetaVersion=15.11.3
baseBetaVersion=15.11.7

View File

@@ -2,7 +2,7 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase">
<application
android:name=".App"
android:icon="@drawable/ic_winboll"
@@ -11,7 +11,7 @@
android:resizeableActivity="true"
android:process=":App">
<activity
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
@@ -29,13 +29,15 @@
</intent-filter>
</activity>
<activity android:name=".GlobalApplication$CrashActivity"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
</application>
</manifest>
</manifest>

View File

@@ -2,6 +2,7 @@ package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,6 +22,8 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
//setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Nov 29 02:40:35 HKT 2025
stageCount=3
#Sun Nov 30 17:12:48 HKT 2025
stageCount=7
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.2
publishVersion=15.11.6
buildCount=0
baseBetaVersion=15.11.3
baseBetaVersion=15.11.7

View File

@@ -3,8 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
<application>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<activity
android:name=".CrashHandler$CrashActivity"
android:label="CrashActivity"
@@ -28,6 +29,22 @@
</activity>
<!-- 崩溃通知复制活动类库Manifest配置确保宿主能合并注册 -->
<activity
android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:taskAffinity=""
android:exported="true"
android:allowTaskReparenting="false"
android:clearTaskOnLaunch="true">
<intent-filter>
<action android:name="cc.winboll.studio.action.COPY_CRASH_LOG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -23,6 +23,7 @@ import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -52,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";
@@ -169,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);
}
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
@@ -185,10 +186,17 @@ public final class CrashHandler {
);
try {
// 启动崩溃页面,终止当前进程(确保完全重启)
app.startActivity(intent);
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
// 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {
// 如果是 release 版,就只发送一个通知
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
}
// 终止当前进程(确保完全重启)
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (ActivityNotFoundException e) {
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
e.printStackTrace();
@@ -428,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);

View File

@@ -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);

View File

@@ -121,6 +121,18 @@ public class ToastUtils {
LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
}
// ===================================== 新增isInited() 方法 =====================================
/**
* 判断 ToastUtils 是否已初始化(供外部调用,如 CrashHandleNotifyUtils 中的复制提示)
* @return true已初始化可正常显示吐司false未初始化/已释放(无法正常显示)
*/
public static boolean isInited() {
ToastUtils instance = getInstance();
// 双重校验1. 未释放 2. 上下文已设置(确保初始化完成)
return !instance.isReleased && instance.mContext != null;
}
// ===================================== 新增结束 =====================================
/**
* 外部接口:显示短时长吐司
* @param message 吐司内容
@@ -243,7 +255,6 @@ public class ToastUtils {
instance.mWorkerThread.join(1000);
} catch (InterruptedException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "线程退出异常", e);
Thread.currentThread().interrupt();
}
instance.mWorkerThread = null;

View File

@@ -0,0 +1,264 @@
package cc.winboll.studio.libappbase.utils;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.GlobalCrashActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
*/
public class CrashHandleNotifyUtils {
public static final String TAG = "CrashHandleNotifyUtils";
/** 通知渠道IDAndroid 8.0+ 必须) */
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
/** 通知渠道名称(用户可见) */
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
/** 通知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+ */
private static final int FLAG_IMMUTABLE = 0x00000040;
/** 通知内容最大行数控制在3行超出部分省略 */
private static final int NOTIFICATION_MAX_LINES = 3;
/**
* 处理未捕获异常(核心方法,类库入口)
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
*/
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 hostAppName = getHostAppName(hostApp, hostPackageName);
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
}
/**
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
* @param hostApp 宿主应用的 Application 实例
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
*/
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);
}
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
// 调用核心方法处理
handleUncaughtException(hostApp, hostPackageName, errorLog);
}
/**
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @return 宿主应用名称(读取失败返回 "未知应用"
*/
private static String getHostAppName(Context hostContext, String hostPackageName) {
try {
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
return hostContext.getPackageManager().getApplicationLabel(
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
).toString();
} catch (Exception e) {
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + "", e);
return "未知应用";
}
}
/**
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
* @param hostContext 宿主应用的上下文Application 实例)
* @param hostPackageName 宿主应用的包名
* @param hostAppName 宿主应用的名称(用于通知标题)
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity
*/
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 失败(包名:" + hostPackageName + "");
return;
}
// 2. 适配 Android 8.0+API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(hostContext, notificationManager);
}
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
if (jumpIntent == null) {
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + "");
return;
}
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
// 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);
}
// 核心用BigTextStyle控制“默认3行省略下拉显示完整”使用宿主上下文构建
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
bigTextStyle.bigText(errorLog);
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
builder.setStyle(bigTextStyle);
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
builder
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
.setSmallIcon(hostContext.getApplicationInfo().icon)
.setContentTitle(hostAppName + " 崩溃")
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
.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 {
return builder.getNotification();
}
}
/**
* 辅助方法截取日志文本确保显示在3行内通用逻辑无包名依赖
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
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 宿主应用的上下文(显式传入,避免类库上下文依赖)
*/
public static void release(Context hostContext) {
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + "");
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M19,21H8V7H19M19,5H8A2,2 0,0 0,6 7V21A2,2 0,0 0,8 23H19A2,2 0,0 0,21 21V7A2,2 0,0 0,19 5M16,1H4A2,2 0,0 0,2 3V17H4V3H16V1Z"/>
</vector>

View File

@@ -4,4 +4,5 @@
<color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color>
<!-- 通知按钮颜色(启用/禁用) -->
</resources>