20251130_124605_319

This commit is contained in:
2025-11-30 12:46:11 +08:00
parent 350118301d
commit d51d693120
11 changed files with 568 additions and 240 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 11:45:13 HKT 2025 #Sun Nov 30 04:34:18 GMT 2025
stageCount=6 stageCount=6
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.11 baseVersion=15.11
publishVersion=15.11.5 publishVersion=15.11.5
buildCount=0 buildCount=8
baseBetaVersion=15.11.6 baseBetaVersion=15.11.6

View File

@@ -22,8 +22,8 @@ public class App extends GlobalApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
//setIsDebugging(false); setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG); //setIsDebugging(BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext()); ToastUtils.init(getApplicationContext());
} }

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 11:43:55 HKT 2025 #Sun Nov 30 04:34:18 GMT 2025
stageCount=6 stageCount=6
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.11 baseVersion=15.11
publishVersion=15.11.5 publishVersion=15.11.5
buildCount=0 buildCount=8
baseBetaVersion=15.11.6 baseBetaVersion=15.11.6

View File

@@ -29,15 +29,16 @@
</activity> </activity>
<!-- 崩溃通知复制动作接收活动(透明无界面 --> <!-- 崩溃通知复制活动类库Manifest配置确保宿主能合并注册 -->
<activity <activity
android:name="cc.winboll.studio.libappbase.CrashCopyReceiverActivity" android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:exported="true"> android:exported="true"
<!-- 注册复制动作的意图过滤器,接收通知按钮点击 --> android:allowTaskReparenting="false"
android:clearTaskOnLaunch="true">
<intent-filter> <intent-filter>
<action android:name="cc.winboll.studio.action.COPY_CRASH_LOG" /> <action android:name="cc.winboll.studio.action.COPY_CRASH_LOG" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@@ -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);
} }
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面) // 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
@@ -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);

View File

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

View File

@@ -1,111 +1,260 @@
package cc.winboll.studio.libappbase.activities; package cc.winboll.studio.libappbase.activities;
import android.app.Activity; import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.Window;
import android.widget.Toast; import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/01 10:00 * @Date 2025/12/01 10:00
* @Describe 崩溃通知复制动作接收活动(透明无界面 * @Describe 崩溃通知复制活动(最终修复版
* 专门接收崩溃通知中“复制日志”按钮的点击事件,处理崩溃日志复制到剪贴板逻辑 * 核心修复:适配双视图(普通+大视图),确保通知悬浮显示+复制按钮正常显示,支持重复点击
* 优势:相比广播接收器,活动在应用崩溃后仍能被系统唤醒,确保复制功能稳定生效 * 适配场景:类库/独立应用Android 4.1+ 全版本,兼容各厂商机型
*/ */
public class CrashCopyReceiverActivity extends Activity { public class CrashCopyReceiverActivity extends Activity {
/** 日志 TAG(标识当前类日志来源) */ /** 日志 TAG */
public static final String TAG = "CrashCopyReceiverActivity"; public static final String TAG = "CrashCopyReceiverActivity";
/** 复制动作 Action需与 CrashHandleNotifyUtils一致) */ /** 复制动作 ActionCrashHandleNotifyUtils完全一致) */
public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG"; public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG";
/** 崩溃日志 Extra 键(需与 CrashHandleNotifyUtils一致) */ /** 崩溃日志 Extra 键(CrashHandleNotifyUtils完全一致) */
public static final String EXTRA_CRASH_LOG = "EXTRA_CRASH_LOG"; 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 = "崩溃日志已复制到剪贴板"; private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板";
/** 活动自动关闭延迟(毫秒):避免占用资源,复制完成后快速关闭 */ /** 按钮禁用后自动恢复延迟1秒防重复点击 */
private static final long AUTO_FINISH_DELAY = 500; 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// 强制设置活动透明(兼容不同主题配置,避免出现白色界面 // 初始化全局Handler仅创建一次避免重复创建导致任务混乱
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mMainHandler = new Handler(Looper.getMainLooper());
getWindow().setFlags( // 强制透明无界面(无闪屏,用户无感知)
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, setTransparentTheme();
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS // 初始化宿主信息(包名、应用名,确保类库场景意图正确)
); initHostInfo();
getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT)); // 处理复制按钮点击逻辑(核心)
}
// 处理复制按钮点击事件
handleCopyAction(getIntent()); handleCopyAction(getIntent());
} }
/** /**
* 处理崩溃日志复制逻辑 * 初始化宿主应用信息(包名、应用名,类库场景关键)
* @param intent 接收的意图(携带崩溃日志) */
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) { private void handleCopyAction(Intent intent) {
// 1. 基础校验(避免非法意图)
if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) { if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
LogUtils.e(TAG, "崩溃日志复制意图,直接关闭活动"); LogUtils.e(TAG, "非复制日志意图,关闭活动");
finish(); finishAndRelease();
return; return;
} }
// 从意图中获取崩溃日志 // 2. 获取崩溃日志和按钮状态(从意图中提取,确保数据正确)
String crashLog = intent.getStringExtra(EXTRA_CRASH_LOG); mCurrentCrashLog = intent.getStringExtra(EXTRA_CRASH_LOG);
if (crashLog == null || crashLog.isEmpty()) { boolean isBtnEnabled = intent.getBooleanExtra(EXTRA_BTN_ENABLED, true);
// 3. 崩溃日志空校验(避免空指针)
if (mCurrentCrashLog == null || mCurrentCrashLog.isEmpty()) {
LogUtils.e(TAG, "崩溃日志为空,无法复制"); LogUtils.e(TAG, "崩溃日志为空,无法复制");
showTip("复制失败:崩溃日志为空"); showTip("复制失败:崩溃日志为空");
finish(); finishAndRelease();
return; return;
} }
// 复制日志到剪贴板 // 4. 按钮状态校验(避免禁用时重复点击)
if (copyTextToClipboard(crashLog)) { if (!isBtnEnabled) {
LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符"); LogUtils.w(TAG, "复制按钮已禁用,忽略本次点击");
finishAndRelease();
return;
}
// 5. 执行复制逻辑适配全版本剪贴板API
if (copyTextToClipboard(mCurrentCrashLog)) {
LogUtils.d(TAG, "崩溃日志复制成功(长度=" + mCurrentCrashLog.length() + "字符)");
showTip(COPY_SUCCESS_TIP); showTip(COPY_SUCCESS_TIP);
// 核心操作1更新通知按钮为【禁用】状态仅更新视图不重复发通知
updateNotificationBtnState(false);
// 核心操作2延迟1秒后恢复按钮【启用】状态支持重复点击
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
updateNotificationBtnState(true);
}
}, BTN_ENABLE_DELAY);
} else { } else {
LogUtils.e(TAG, "崩溃日志复制失败"); LogUtils.e(TAG, "崩溃日志复制失败");
showTip("复制失败,请重试"); showTip("复制失败,请重试");
} }
// 延迟关闭活动(确保提示能正常显示,用户感知) // 6. 关闭透明活动(用户感知,释放资源
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { finishAndRelease();
@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 需复制的文本(崩溃日志) * 调用工具类更新 RemoteViews普通视图+大视图),不重复发送通知
* @return true复制成功false复制失败 * @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) { private boolean copyTextToClipboard(String text) {
try { try {
// 适配 Android 11+API 30+)剪贴板 API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); // 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); android.content.ClipData clipData = android.content.ClipData.newPlainText("崩溃日志", text);
clipboard.setPrimaryClip(clipData); clipboard.setPrimaryClip(clipData);
} else { } else {
// 适配 Android 10 及以下版本剪贴板 API // Android 10及以下剪贴板API(适配低版本)
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard == null) return false;
clipboard.setText(text); clipboard.setText(text);
} }
return true; return true;
} catch (SecurityException e) {
// 类库场景关键:捕获宿主剪贴板权限异常(部分机型限制类库访问)
LogUtils.e(TAG, "复制失败:宿主剪贴板权限被拒绝", e);
return false;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "复制文本到剪贴板失败", e); LogUtils.e(TAG, "复制文本到剪贴板失败", e);
return false; return false;
@@ -113,17 +262,17 @@ public class CrashCopyReceiverActivity extends Activity {
} }
/** /**
* 显示提示信息(优先使用 ToastUtils失败降级系统 Toast * 显示提示(优先使用项目封装的ToastUtils失败降级系统Toast,确保提示正常显示
* @param tip 提示内容 * @param tip 提示内容
*/ */
private void showTip(String tip) { private void showTip(String tip) {
try { try {
// 优先使用项目封装的 ToastUtils确保样式统一 // 优先使用ToastUtils确保样式统一
if (ToastUtils.isInited()) { if (ToastUtils.class != null && ToastUtils.isInited()) {
ToastUtils.show(tip); ToastUtils.show(tip);
} else { } else {
// 降级使用系统 Toast确保提示能正常显示 // 降级使用系统Toast避免ToastUtils未初始化导致提示失败
Toast.makeText(this, tip, Toast.LENGTH_SHORT).show(); Toast.makeText(getApplicationContext(), tip, Toast.LENGTH_SHORT).show();
} }
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "显示提示失败", e); LogUtils.e(TAG, "显示提示失败", e);
@@ -131,21 +280,51 @@ public class CrashCopyReceiverActivity extends Activity {
} }
/** /**
* 处理重复意图(避免多次触发复制 * 统一关闭活动并释放资源(避免内存泄漏,确保资源回收
*/
private void finishAndRelease() {
// 关闭透明活动
finish();
// Android 12+ 移除任务栈,避免留在最近任务列表
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
finishAndRemoveTask();
}
// 清空意图,避免重复处理
setIntent(null);
}
/**
* 处理重复点击(更新意图并重新执行复制逻辑,确保每次点击都有效)
*/ */
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
setIntent(intent); // 更新当前意图 if (intent != null && ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
handleCopyAction(intent); // 重新处理复制动作 setIntent(intent); // 更新为最新意图(确保获取最新日志和按钮状态)
handleCopyAction(intent); // 重新执行复制逻辑
}
} }
/** /**
* 禁止活动旋转时重建(避免复制逻辑重复执行) * 禁止活动旋转时重建(避免复制逻辑重复执行,提升稳定性
*/ */
@Override @Override
public void onConfigurationChanged(android.content.res.Configuration newConfig) { public void onConfigurationChanged(android.content.res.Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
setIntent(null); // 清空意图,避免旋转后重复处理
}
/**
* 活动销毁时释放资源(彻底清理,避免内存泄漏)
*/
@Override
protected void onDestroy() {
super.onDestroy();
finishAndRelease();
// 清空Handler所有任务避免延迟任务导致内存泄漏
if (mMainHandler != null) {
mMainHandler.removeCallbacksAndMessages(null);
}
} }
} }

View File

@@ -7,68 +7,109 @@ 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 android.widget.RemoteViews;
import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity; import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity;
import cc.winboll.studio.libappbase.CrashHandler; import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R; import cc.winboll.studio.libappbase.R;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 崩溃通知工具集优化2行摘要+按钮防溢出)
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集(类库专用,支持宿主应用唤醒活动)
* 核心功能应用崩溃时捕获错误日志发送通知到系统通知栏3行内容省略+复制按钮点击复制按钮唤醒CrashCopyReceiverActivity完成日志拷贝
*/ */
public class CrashHandleNotifyUtils { public class CrashHandleNotifyUtils {
public static final String TAG = "CrashHandleNotifyUtils"; public static final String TAG = "CrashHandleNotifyUtils";
/** 通知渠道IDAndroid 8.0+ 必须,用于归类通知 */ /** 通知渠道IDAndroid 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行超出部分省略 */ /** 通知布局ID普通+大视图 */
private static final int NOTIFICATION_MAX_LINES = 3; private static final int NOTIFICATION_LAYOUT_NORMAL = R.layout.layout_crash_notification_normal; // 通知栏2行摘要
/** 复制按钮请求码(区分多个 PendingIntent */ private static final int NOTIFICATION_LAYOUT_BIG = R.layout.layout_crash_notification_big; // 悬浮/下拉(完整日志)
private static final int REQUEST_CODE_COPY = 0x002; /** 按钮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);
/** /**
* 处理未捕获异常(核心方法,类库场景专用:强制使用宿主应用包名构建意图 * 处理未捕获异常(核心生成2行摘要
* 1. 提取应用名称和崩溃日志;
* 2. 创建并发送系统通知3行内容省略+复制按钮);
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
* @param app 应用全局 Application 实例(宿主应用的 Application确保包名正确
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
*/ */
public static void handleUncaughtException(Application app, Intent intent) { public static void handleUncaughtException(Application app, Intent intent) {
// 1. 提取应用名称(优化:从宿主 Application 中获取真实应用名)
String appName = getAppName(app); String appName = getAppName(app);
// 2. 提取崩溃日志(从 Intent Extra 中获取,对应 CrashHandler 存储的崩溃信息) String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
// 校验参数(避免空指针,确保通知正常发送)
if (app == null || appName == null || errorLog == null) { if (app == null || appName == null || errorLog == null) {
LogUtils.e(TAG, "发送崩溃通知失败:参数为空app=" + app + ", appName=" + appName + ", errorLog=" + errorLog + ""); LogUtils.e(TAG, "发送崩溃通知失败:参数为空");
return; return;
} }
// 3. 发送崩溃通知到通知栏(类库场景:强制用宿主包名构建意图 // 核心优化生成2行摘要异常类型 + 触发时间
sendCrashNotification(app, appName, errorLog, app.getPackageName()); String crashSummary = getCrash2LineSummary(errorLog);
// 发送通知(传入摘要用于通知栏,完整日志用于大视图)
sendCrashNotification(app, appName, crashSummary, errorLog, app.getPackageName());
} }
/** /**
* 获取应用真实名称(从宿主 AndroidManifest 中读取 android:label * 核心方法提取崩溃日志的2行摘要异常类型 + 触发时间
* @param context 上下文(宿主 Application 实例,确保获取正确的应用名称) * @param errorLog 完整崩溃日志
* @return 应用名称(读取失败返回 "未知应用" * @return 2行字符串第一行异常类型第二行触发时间
*/ */
private static String getCrash2LineSummary(String errorLog) {
if (TextUtils.isEmpty(errorLog)) {
return "未知异常\n" + getCurrentTime();
}
// 第一行提取异常类型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();
}
}
}
}
// 第二行当前时间格式HH:mm:ss
String timeStr = getCurrentTime();
// 组合为2行摘要确保仅2行无多余内容
return errorType + "\n" + timeStr;
}
/**
* 获取当前时间格式HH:mm:ss
*/
private static String getCurrentTime() {
return "触发时间:" + DATE_FORMAT.format(new Date());
}
private static String getAppName(Context context) { private static String getAppName(Context context) {
try { try {
// 从宿主包管理器中获取应用信息(类库场景必须用宿主上下文)
return context.getPackageManager().getApplicationLabel( return context.getPackageManager().getApplicationLabel(
context.getApplicationInfo() context.getApplicationInfo()
).toString(); ).toString();
@@ -79,208 +120,219 @@ public class CrashHandleNotifyUtils {
} }
/** /**
* 发送崩溃通知到系统通知栏(类库专用:新增宿主包名参数,确保意图跳转正确 * 发送通知优化通知栏显示2行摘要大视图显示完整日志
* @param context 上下文(宿主 Application 实例) * @param context 上下文
* @param title 通知标题(应用名称) * @param title 通知标题
* @param content 通知内容(崩溃日志 * @param crashSummary 2行摘要通知栏显示
* @param hostPackageName 宿主应用包名(关键:用于构建跨类库的活动意图 * @param fullErrorLog 完整日志(大视图显示
* @param hostPackageName 宿主包名
*/ */
private static void sendCrashNotification(Context context, String title, String content, String hostPackageName) { private static void sendCrashNotification(Context context, String title, String crashSummary,
// 1. 获取通知管理器(系统服务,用于发送/管理通知) String fullErrorLog, String hostPackageName) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) { if (notificationManager == null) {
LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空"); LogUtils.e(TAG, "获取NotificationManager失败");
return; return;
} }
// 2. 适配 Android 8.0+API 26+):创建通知渠道(必须,否则通知不显示 // 1. 适配Android 8.0+通知渠道(高重要性,支持悬浮
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(notificationManager); createCrashNotifyChannel(notificationManager);
} }
// 3. 构建通知意图(类库场景:用宿主包名构建意图,确保跳转成功 // 2. 构建双视图普通视图2行摘要大视图完整日志
PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); // 主界面跳转意图 RemoteViews remoteViewsNormal = createRemoteViews(context, title, crashSummary, true, NOTIFICATION_LAYOUT_NORMAL);
PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, hostPackageName); // 唤醒复制活动的意图 RemoteViews remoteViewsBig = createRemoteViews(context, title, fullErrorLog, true, NOTIFICATION_LAYOUT_BIG);
// 4. 构建通知实例核心修复3行内容省略+复制按钮) // 3. 构建意图
Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent); PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName);
PendingIntent copyPendingIntent = getCopyPendingIntent(context, fullErrorLog, hostPackageName, true);
// 5. 发送通知(指定通知ID重复发送同ID会覆盖原通知 // 4. 构建通知(优化:确保通知栏按钮完整显示
Notification.Builder builder = new Notification.Builder(context);
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);
}
builder
.setSmallIcon(context.getApplicationInfo().icon) // 必需:小图标
.setContentIntent(launchPendingIntent)
.setAutoCancel(true) // 点击关闭,不常驻(避免长期占用通知栏)
.setOngoing(false)
.setWhen(System.currentTimeMillis())
.setTicker("应用崩溃:" + getCrash2LineSummary(fullErrorLog).split("\n")[0]); // 滚动提示:仅显示异常类型
// 5. 构建并发送通知
Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
? builder.build()
: builder.getNotification();
notificationManager.notify(CRASH_NOTIFY_ID, notification); notificationManager.notify(CRASH_NOTIFY_ID, notification);
LogUtils.d(TAG, "崩溃通知发送成功(类库场景):标题=" + title + ",宿主包名=" + hostPackageName); LogUtils.d(TAG, "崩溃通知发送成功(2行摘要+按钮完整显示)");
} }
/** /**
* 创建崩溃通知渠道Android 8.0+ 必需 * 构建RemoteViews适配双视图
* @param notificationManager 通知管理器
*/ */
private static void createCrashNotifyChannel(NotificationManager notificationManager) { private static RemoteViews createRemoteViews(Context context, String title, String content, boolean isBtnEnabled, int layoutId) {
// 仅 Android 8.0+ 执行(避免低版本报错) RemoteViews remoteViews = new RemoteViews(context.getPackageName(), layoutId);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 构建通知渠道指定ID、名称、重要性 // 设置标题/内容(普通视图=2行摘要大视图=完整日志
android.app.NotificationChannel channel = new android.app.NotificationChannel( remoteViews.setTextViewText(TV_TITLE_ID, title + " 崩溃"); // 标题优化:增加"崩溃"标识
CRASH_NOTIFY_CHANNEL_ID, remoteViews.setTextViewText(TV_CONTENT_ID, content);
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示) // 绑定按钮点击意图
); PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, context.getPackageName(), isBtnEnabled);
// 可选:设置渠道描述(用户在设置中可见) remoteViews.setOnClickPendingIntent(BTN_COPY_ID, copyPendingIntent);
channel.setDescription("用于显示应用崩溃信息,支持复制日志");
// 注册通知渠道到系统 // 按钮状态+颜色
notificationManager.createNotificationChannel(channel); remoteViews.setBoolean(BTN_COPY_ID, "setEnabled", isBtnEnabled);
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID); 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);
} }
} }
/** /**
* 建通知点击跳转意图(跳转宿主应用主界面,类库场景专用 * 建通知渠道(高重要性,支持悬浮
* @param context 上下文(宿主 Application 实例) */
* @param hostPackageName 宿主应用包名(关键:确保跳转宿主主界面) private static void createCrashNotifyChannel(NotificationManager notificationManager) {
* @return 主界面跳转 PendingIntent 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 PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) { private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) {
// 1. 获取宿主应用主界面 Intent强制用宿主包名获取避免类库包名干扰
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName); Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName);
if (launchIntent == null) { if (launchIntent == null) {
// 异常处理:若主界面 Intent 为空,创建空意图(避免崩溃)
launchIntent = new Intent(); launchIntent = new Intent();
} }
// 2. 构建 PendingIntent延迟执行的意图类库场景增加 FLAG_ONE_SHOT 确保单次有效)
int flags = PendingIntent.FLAG_UPDATE_CURRENT; int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 适配 Android 12+API 31+):添加 FLAG_IMMUTABLE 避免安全警告
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, context,
0, // 请求码(可忽略) 0,
launchIntent, launchIntent,
flags flags
); );
} }
/** /**
* 构建复制按钮意图(类库场景专用:用宿主包名唤醒活动,确保跳转成功) * 构建复制按钮意图
* @param context 上下文(宿主 Application 实例)
* @param errorLog 崩溃日志(需要复制的内容)
* @param hostPackageName 宿主应用包名(关键:确保系统能找到类库中的活动)
* @return 唤醒复制活动的 PendingIntent
*/ */
private static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName) { public static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName, boolean isEnabled) {
// 1. 构建唤醒 CrashCopyReceiverActivity 的显式意图(类库场景关键:指定宿主包名)
Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class); Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class);
// 强制设置意图的包名为宿主包名(解决类库与宿主包名不匹配问题)
copyIntent.setPackage(hostPackageName); copyIntent.setPackage(hostPackageName);
// 设置动作与Activity中匹配确保意图精准匹配
copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG); copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG);
// 携带完整崩溃日志键与Activity中一致
copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog); copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog);
// 类库场景增强标志:确保活动在宿主应用中能被唤醒,且不重复创建 copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_BTN_ENABLED, isEnabled);
copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_NO_HISTORY); // 不保留活动历史,避免残留
// 2. 构建 PendingIntent类库场景使用 FLAG_ONE_SHOT 避免重复触发) int dynamicRequestCode = CRASH_NOTIFY_ID + (int) (System.currentTimeMillis() % 1000);
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; int flags = PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT;
// 适配 Android 12+API 31+):添加 FLAG_IMMUTABLE 避免安全警告
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE; flags |= FLAG_IMMUTABLE;
} }
// 3. 返回活动类型的 PendingIntent类库场景用宿主上下文构建确保权限通过
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,
REQUEST_CODE_COPY, // 唯一请求码,区分主界面意图 dynamicRequestCode,
copyIntent, copyIntent,
flags flags
); );
} }
/** /**
* 构建通知实例核心修复3行内容省略+复制按钮) * 关闭通知
* @param context 上下文(宿主 Application 实例)
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
* @param launchPendingIntent 通知点击跳转意图
* @param copyPendingIntent 唤醒复制活动的意图
* @return 构建完成的 Notification 对象
*/ */
private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent, PendingIntent copyPendingIntent) { public static void cancelCrashNotification(Context context) {
// 兼容 Android 8.0+指定通知渠道ID NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = new Notification.Builder(context); if (notificationManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationManager.cancel(CRASH_NOTIFY_ID);
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
}
// 核心修复1用BigTextStyle控制“默认3行省略下拉显示完整”
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); // 底部省略提示
bigTextStyle.bigText(content); // 完整日志(下拉时显示)
bigTextStyle.setBigContentTitle(title); // 下拉后的标题(与主标题一致)
builder.setStyle(bigTextStyle);
// 核心修改2添加复制按钮Android 4.1+ 支持通知按钮,类库场景用系统图标避免资源缺失)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// 类库场景:用系统默认复制图标(避免自定义图标在宿主应用中缺失)
int copyIcon = R.drawable.ic_content_copy;
// 若类库有自定义图标,优先使用(需确保宿主应用能访问类库资源)
if (R.drawable.ic_content_copy != 0) {
copyIcon = R.drawable.ic_content_copy;
}
builder.addAction(
copyIcon, // 兼容图标(系统图标兜底)
"复制日志", // 按钮文本
copyPendingIntent // 按钮点击意图唤醒CrashCopyReceiverActivity
);
}
// 配置通知核心参数移除setLines避免报错
builder
.setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(用宿主应用图标,避免类库图标缺失)
.setContentTitle(title) // 通知主标题(应用名称)
.setContentText(getShortContent(content)) // 核心3行内缩略文本
.setContentIntent(launchPendingIntent) // 通知主体点击跳转主界面
.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 {
// Android 4.0 及以下版本,使用 getNotification() 方法
return builder.getNotification();
} }
} }
/**
* 辅助方法截取日志文本确保显示在3行内按字符数估算适配大多数设备
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
private static String getShortContent(String content) {
if (content == null || content.isEmpty()) {
return "无崩溃日志";
}
// 估算3行字符数80字符可根据设备屏幕调整避免因字符过长导致换行超3行
int maxLength = 80;
if (content.length() <= maxLength) {
return content; // 不足3行直接返回完整文本
} else {
// 超出3行截取前80字符并加省略号确保视觉上仅显示3行
return content.substring(0, maxLength) + "...";
}
}
/**
* 释放资源(删除原广播注销逻辑,仅保留空实现便于兼容旧代码调用)
* @param context 上下文(宿主 Application 实例)
*/
public static void release(Context context) { public static void release(Context context) {
// 因已移除广播接收器,此处仅保留空实现,避免调用方报错 LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成");
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(类库场景,无广播接收器需注销)");
} }
} }

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 崩溃通知大视图布局(用于悬浮通知、下拉展开时显示) -->
<!-- 与普通视图layout_crash_notification_normal.xml控件ID完全一致确保RemoteViews更新同步 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="@android:color/white">
<!-- 通知标题与普通视图ID一致tv_crash_title -->
<TextView
android:id="@+id/tv_crash_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<!-- 通知内容大视图显示完整日志无行数限制与普通视图ID一致tv_crash_content -->
<TextView
android:id="@+id/tv_crash_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="8dp" />
<!-- 复制按钮大视图按钮尺寸稍大提升点击体验与普通视图ID一致btn_crash_copy -->
<Button
android:id="@+id/btn_crash_copy"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:text="复制日志"
android:textSize="14sp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:background="@android:drawable/btn_default"
android:textColor="@color/color_btn_enabled" />
</LinearLayout>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 通知栏普通视图核心优化2行摘要内容+按钮防溢出,适配所有机型通知栏) -->
<!-- 仅显示:异常类型 + 触发时间2行按钮独立一行确保完整显示 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp"
android:background="@android:color/white"
android:gravity="start"> <!-- 控件左对齐,避免右侧溢出 -->
<!-- 通知标题(强制单行,简洁标识) -->
<TextView
android:id="@+id/tv_crash_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="@android:color/black"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginBottom="2dp" />
<!-- 核心2行摘要内容异常类型 + 触发时间强制2行无多余内容 -->
<TextView
android:id="@+id/tv_crash_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="@android:color/darker_gray"
android:maxLines="2"
android:ellipsize="end"
android:lineSpacingExtra="1dp"
android:layout_marginBottom="4dp" />
<!-- 复制按钮(独立一行,宽高适配,确保完整显示) -->
<Button
android:id="@+id/btn_crash_copy"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="复制日志"
android:textSize="11sp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:background="@android:drawable/btn_default_small"
android:textColor="@color/color_btn_enabled"
android:layout_marginTop="2dp" /> <!-- 与内容区留极小间距,节省空间 -->
</LinearLayout>

View File

@@ -4,4 +4,7 @@
<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>
<!-- 通知按钮颜色(启用/禁用) -->
<color name="color_btn_enabled">#0066CC</color> <!-- 蓝色:启用状态 -->
<color name="color_btn_disabled">#999999</color> <!-- 灰色:禁用状态 -->
</resources> </resources>