Compare commits
18 Commits
appbase-v1
...
appbase
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bb70fae6e | |||
| 4b11f27d09 | |||
| 12dc8eef6b | |||
| 8930c43bcd | |||
| a138635cae | |||
| ad6d29c27e | |||
| 8aafcabba9 | |||
| fc34ed0d5a | |||
| d51d693120 | |||
| 350118301d | |||
| e47a64cc87 | |||
| 65161b1a80 | |||
| aa24bc5e11 | |||
| 1b24fc99ef | |||
| be6b7841ed | |||
| e4dc8109aa | |||
| d0e818056a | |||
| c744289896 |
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Nov 29 21:41:11 HKT 2025
|
#Sun Nov 30 17:13:07 HKT 2025
|
||||||
stageCount=4
|
stageCount=7
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.11
|
baseVersion=15.11
|
||||||
publishVersion=15.11.3
|
publishVersion=15.11.6
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.11.4
|
baseBetaVersion=15.11.7
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="cc.winboll.studio.appbase">
|
package="cc.winboll.studio.appbase">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:icon="@drawable/ic_winboll"
|
android:icon="@drawable/ic_winboll"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:process=":App">
|
android:process=":App">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -29,13 +29,15 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.max_aspect"
|
android:name="android.max_aspect"
|
||||||
android:value="4.0"/>
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -22,6 +22,7 @@ public class App extends GlobalApplication {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
|
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
|
||||||
|
//setIsDebugging(false);
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||||
ToastUtils.init(getApplicationContext());
|
ToastUtils.init(getApplicationContext());
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Nov 29 21:41:11 HKT 2025
|
#Sun Nov 30 17:12:48 HKT 2025
|
||||||
stageCount=4
|
stageCount=7
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.11
|
baseVersion=15.11
|
||||||
publishVersion=15.11.3
|
publishVersion=15.11.6
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.11.4
|
baseBetaVersion=15.11.7
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="cc.winboll.studio.libappbase">
|
package="cc.winboll.studio.libappbase">
|
||||||
|
|
||||||
<application>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".CrashHandler$CrashActivity"
|
android:name=".CrashHandler$CrashActivity"
|
||||||
android:label="CrashActivity"
|
android:label="CrashActivity"
|
||||||
@@ -28,6 +29,22 @@
|
|||||||
|
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public final class CrashHandler {
|
|||||||
public static final String TITTLE = "CrashReport";
|
public static final String TITTLE = "CrashReport";
|
||||||
|
|
||||||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
||||||
public static final String EXTRA_CRASH_INFO = "crashInfo";
|
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||||||
|
|
||||||
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
||||||
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
||||||
@@ -170,12 +170,12 @@ public final class CrashHandler {
|
|||||||
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
||||||
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
||||||
intent.setClass(app, GlobalCrashActivity.class);
|
intent.setClass(app, GlobalCrashActivity.class);
|
||||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志
|
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
|
||||||
} else {
|
} else {
|
||||||
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
||||||
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
||||||
intent.setClass(app, CrashActivity.class);
|
intent.setClass(app, CrashActivity.class);
|
||||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog);
|
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
||||||
@@ -186,7 +186,7 @@ public final class CrashHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (GlobalApplication.isDebugging()) {
|
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||||
// 如果是 debug 版,启动崩溃页面窗口
|
// 如果是 debug 版,启动崩溃页面窗口
|
||||||
app.startActivity(intent);
|
app.startActivity(intent);
|
||||||
} else {
|
} else {
|
||||||
@@ -436,7 +436,7 @@ public final class CrashHandler {
|
|||||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||||
|
|
||||||
// 获取传递的崩溃日志
|
// 获取传递的崩溃日志
|
||||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO);
|
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||||
// 设置系统默认主题(避免自定义主题冲突)
|
// 设置系统默认主题(避免自定义主题冲突)
|
||||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
|
|||||||
.postResumeCrashSafetyWireHandler(getApplicationContext());
|
.postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||||
|
|
||||||
// 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
|
// 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
|
||||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
|
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||||
|
|
||||||
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
|
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
|
||||||
setContentView(R.layout.activity_globalcrash);
|
setContentView(R.layout.activity_globalcrash);
|
||||||
|
|||||||
@@ -121,6 +121,18 @@ public class ToastUtils {
|
|||||||
LogUtils.d(TAG, "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 吐司内容
|
* @param message 吐司内容
|
||||||
@@ -243,7 +255,6 @@ public class ToastUtils {
|
|||||||
instance.mWorkerThread.join(1000);
|
instance.mWorkerThread.join(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||||
//LogUtils.e(TAG, "线程退出异常", e);
|
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
instance.mWorkerThread = null;
|
instance.mWorkerThread = null;
|
||||||
|
|||||||
@@ -7,189 +7,258 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.CrashHandler;
|
import cc.winboll.studio.libappbase.CrashHandler;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalCrashActivity;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
* @Date 2025/11/29 21:12
|
* @Date 2025/11/29 21:12
|
||||||
* @Describe 应用崩溃处理通知实用工具集
|
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
|
||||||
* 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏,方便用户查看崩溃信息
|
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
|
||||||
|
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
|
||||||
*/
|
*/
|
||||||
public class CrashHandleNotifyUtils {
|
public class CrashHandleNotifyUtils {
|
||||||
|
|
||||||
public static final String TAG = "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_ID = "crash_notify_channel";
|
||||||
/** 通知渠道名称(用户可见,描述渠道用途) */
|
/** 通知渠道名称(用户可见) */
|
||||||
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
|
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
|
||||||
/** 通知ID(唯一标识一条通知,避免重复创建) */
|
/** 通知ID(唯一) */
|
||||||
private static final int CRASH_NOTIFY_ID = 0x001;
|
public static final int CRASH_NOTIFY_ID = 0x001;
|
||||||
/** Android 12 对应 API 版本号(31),替代 Build.VERSION_CODES.S */
|
/** Android 12 对应 API 版本号(31) */
|
||||||
private static final int API_LEVEL_ANDROID_12 = 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;
|
private static final int FLAG_IMMUTABLE = 0x00000040;
|
||||||
|
|
||||||
/**
|
/** 通知内容最大行数(控制在3行,超出部分省略) */
|
||||||
* 处理未捕获异常(核心方法)
|
private static final int NOTIFICATION_MAX_LINES = 3;
|
||||||
* 1. 提取应用名称和崩溃日志;
|
|
||||||
* 2. 创建并发送系统通知(标题:应用名称,内容:崩溃日志);
|
|
||||||
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
|
|
||||||
* @param app 应用全局 Application 实例(用于获取上下文、应用信息)
|
|
||||||
* @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 校验参数(避免空指针,确保通知正常发送)
|
|
||||||
if (app == null || appName == null || errorLog == null) {
|
/**
|
||||||
LogUtils.e(TAG, "发送崩溃通知失败:参数为空(app=" + app + ", appName=" + appName + ", errorLog=" + errorLog + ")");
|
* 处理未捕获异常(核心方法,类库入口)
|
||||||
|
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
|
||||||
|
* @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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发送崩溃通知到通知栏
|
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
|
||||||
sendCrashNotification(app, appName, errorLog);
|
String hostAppName = getHostAppName(hostApp, hostPackageName);
|
||||||
|
|
||||||
|
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity)
|
||||||
|
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取应用真实名称(从 AndroidManifest 中读取 android:label)
|
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
|
||||||
* 替代原 app.getClass().toString()(原方式获取的是类名,用户不可读)
|
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
|
||||||
* @param context 上下文(Application 实例)
|
* @param hostApp 宿主应用的 Application 实例
|
||||||
* @return 应用名称(读取失败返回 "未知应用")
|
* @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
|
||||||
*/
|
*/
|
||||||
private static String getAppName(Context context) {
|
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 {
|
try {
|
||||||
// 从包管理器中获取应用信息(包含应用名称)
|
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
|
||||||
return context.getPackageManager().getApplicationLabel(
|
return hostContext.getPackageManager().getApplicationLabel(
|
||||||
context.getApplicationInfo()
|
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
|
||||||
).toString();
|
).toString();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LogUtils.e(TAG, "获取应用名称失败", e);
|
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e);
|
||||||
return "未知应用";
|
return "未知应用";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送崩溃通知到系统通知栏
|
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
|
||||||
* @param context 上下文(Application 实例,确保后台也能发送)
|
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
|
||||||
* @param title 通知标题(应用名称)
|
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||||
* @param content 通知内容(崩溃日志)
|
* @param hostPackageName 宿主应用的包名
|
||||||
|
* @param hostAppName 宿主应用的名称(用于通知标题)
|
||||||
|
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||||
*/
|
*/
|
||||||
private static void sendCrashNotification(Context context, String title, String content) {
|
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
|
||||||
// 1. 获取通知管理器(系统服务,用于发送/管理通知)
|
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
if (notificationManager == null) {
|
if (notificationManager == null) {
|
||||||
LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空");
|
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 适配 Android 8.0+(API 26+):创建通知渠道(必须,否则通知不显示)
|
// 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
createCrashNotifyChannel(notificationManager);
|
createCrashNotifyChannel(hostContext, notificationManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 构建通知意图(点击通知时可跳转,此处默认跳转应用主界面,可自定义)
|
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
|
||||||
PendingIntent pendingIntent = getNotificationPendingIntent(context);
|
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
|
||||||
|
if (jumpIntent == null) {
|
||||||
|
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 构建通知实例(兼容低版本,使用 Notification.Builder 构建)
|
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
|
||||||
Notification notification = buildNotification(context, title, content, pendingIntent);
|
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
|
||||||
|
|
||||||
// 5. 发送通知(指定通知ID,重复发送同ID会覆盖原通知)
|
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
|
||||||
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
||||||
LogUtils.d(TAG, "崩溃通知发送成功:标题=" + title + ",内容长度=" + content.length() + "字符");
|
LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建崩溃通知渠道(Android 8.0+ 必需)
|
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突)
|
||||||
* 通知渠道用于归类通知,用户可在系统设置中管理(开启/关闭/静音)
|
* @param hostContext 宿主应用的上下文
|
||||||
* @param notificationManager 通知管理器
|
* @param notificationManager 宿主的通知管理器
|
||||||
*/
|
*/
|
||||||
private static void createCrashNotifyChannel(NotificationManager notificationManager) {
|
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
|
||||||
// 仅 Android 8.0+ 执行(避免低版本报错)
|
// 仅 Android 8.0+ 执行(避免低版本报错)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// 构建通知渠道(指定ID、名称、重要性)
|
// 构建通知渠道(归属宿主应用,描述明确类库用途)
|
||||||
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
||||||
CRASH_NOTIFY_CHANNEL_ID,
|
CRASH_NOTIFY_CHANNEL_ID,
|
||||||
CRASH_NOTIFY_CHANNEL_NAME,
|
CRASH_NOTIFY_CHANNEL_NAME,
|
||||||
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
);
|
);
|
||||||
// 可选:设置渠道描述(用户在设置中可见)
|
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
|
||||||
channel.setDescription("用于显示应用崩溃信息,帮助定位问题");
|
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
|
||||||
// 注册通知渠道到系统
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID);
|
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建通知点击意图(PendingIntent)
|
* 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
|
||||||
* 点击通知后跳转应用主界面(可根据需求修改为跳转崩溃日志详情页)
|
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity;
|
||||||
* @param context 上下文
|
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
|
||||||
* @return 封装好的 PendingIntent(用于通知点击跳转)
|
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
|
||||||
|
* @param hostContext 宿主应用的上下文
|
||||||
|
* @param hostPackageName 宿主应用的包名
|
||||||
|
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||||
|
* @return 跳转崩溃详情页的 PendingIntent
|
||||||
*/
|
*/
|
||||||
private static PendingIntent getNotificationPendingIntent(Context context) {
|
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
|
||||||
// 1. 获取应用主界面 Intent(从包名启动默认 launcher Activity)
|
try {
|
||||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(
|
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
|
||||||
context.getPackageName()
|
Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
|
||||||
);
|
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰)
|
||||||
if (launchIntent == null) {
|
crashIntent.setPackage(hostPackageName);
|
||||||
// 异常处理:若主界面 Intent 为空,创建空意图(避免崩溃)
|
// 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配)
|
||||||
launchIntent = new Intent();
|
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
|
||||||
}
|
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
|
||||||
|
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
|
||||||
// 2. 构建 PendingIntent(延迟执行的意图,FLAG_UPDATE_CURRENT 表示更新已存在的意图)
|
// 2. 构建 PendingIntent(使用宿主上下文,适配高版本)
|
||||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
// 适配 Android 12+(API 31+):添加 FLAG_IMMUTABLE 避免安全警告(用常量值替代高版本 API)
|
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
flags |= FLAG_IMMUTABLE;
|
||||||
flags |= FLAG_IMMUTABLE;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context,
|
hostContext,
|
||||||
0, // 请求码(可忽略,用于区分多个 PendingIntent)
|
CRASH_NOTIFY_ID, // 用通知ID作为请求码,确保唯一(避免意图复用)
|
||||||
launchIntent,
|
crashIntent,
|
||||||
flags
|
flags
|
||||||
);
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建通知实例(兼容 Android 4.0+ 所有版本)
|
* 构建通知实例(类库兼容版)
|
||||||
* @param context 上下文
|
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
|
||||||
* @param title 通知标题
|
* @param hostContext 宿主应用的上下文
|
||||||
* @param content 通知内容
|
* @param hostAppName 宿主应用的名称(通知标题)
|
||||||
* @param pendingIntent 通知点击意图
|
* @param errorLog 崩溃日志(通知内容)
|
||||||
|
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity)
|
||||||
* @return 构建完成的 Notification 对象
|
* @return 构建完成的 Notification 对象
|
||||||
*/
|
*/
|
||||||
private static Notification buildNotification(Context context, String title, String content, PendingIntent pendingIntent) {
|
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
|
||||||
// 兼容 Android 8.0+:指定通知渠道ID
|
// 兼容 Android 8.0+:指定宿主的通知渠道ID
|
||||||
Notification.Builder builder = new Notification.Builder(context);
|
Notification.Builder builder = new Notification.Builder(hostContext);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
|
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
|
builder
|
||||||
.setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(必需,从应用图标获取)
|
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
|
||||||
.setContentTitle(title) // 通知标题(应用名称)
|
.setSmallIcon(hostContext.getApplicationInfo().icon)
|
||||||
.setContentText(content) // 通知内容(崩溃日志)
|
.setContentTitle(hostAppName + " 崩溃")
|
||||||
.setContentIntent(pendingIntent) // 通知点击意图
|
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
|
||||||
.setAutoCancel(true) // 点击通知后自动取消
|
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
|
||||||
.setWhen(System.currentTimeMillis()) // 通知创建时间(当前时间)
|
.setAutoCancel(true) // 点击后自动关闭
|
||||||
.setPriority(Notification.PRIORITY_DEFAULT); // 通知优先级(默认)
|
.setWhen(System.currentTimeMillis())
|
||||||
|
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||||
|
|
||||||
// 可选:长文本适配(当崩溃日志过长时,显示完整文本)
|
// 适配 Android 4.1+:确保在宿主应用中正常显示
|
||||||
if (content.length() > 100) { // 超过100字符时,设置长文本样式
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
|
return builder.build();
|
||||||
bigTextStyle.bigText(content); // 显示完整崩溃日志
|
} else {
|
||||||
builder.setStyle(bigTextStyle);
|
return builder.getNotification();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建通知并返回(getNotification() 兼容低版本,build() 是 Android 4.1+ 方法)
|
/**
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? builder.build() : 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() : "未知") + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
libappbase/src/main/res/drawable/ic_content_copy.xml
Normal file
11
libappbase/src/main/res/drawable/ic_content_copy.xml
Normal 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>
|
||||||
@@ -4,4 +4,5 @@
|
|||||||
<color name="colorPrimaryDark">#FF005C12</color>
|
<color name="colorPrimaryDark">#FF005C12</color>
|
||||||
<color name="colorAccent">#FF8DFFA2</color>
|
<color name="colorAccent">#FF8DFFA2</color>
|
||||||
<color name="colorText">#FFFFFB8D</color>
|
<color name="colorText">#FFFFFB8D</color>
|
||||||
|
<!-- 通知按钮颜色(启用/禁用) -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user