回退到应用崩溃通知悬浮窗可用版本,并且修改通知跳转窗口。
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Nov 30 04:34:18 GMT 2025
|
||||
#Sun Nov 30 09:00:20 GMT 2025
|
||||
stageCount=6
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.5
|
||||
buildCount=8
|
||||
buildCount=9
|
||||
baseBetaVersion=15.11.6
|
||||
|
||||
@@ -186,7 +186,7 @@ public final class CrashHandler {
|
||||
);
|
||||
|
||||
try {
|
||||
if (GlobalApplication.isDebugging()) {
|
||||
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||
// 如果是 debug 版,启动崩溃页面窗口
|
||||
app.startActivity(intent);
|
||||
} else {
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
package cc.winboll.studio.libappbase.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Window;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/01 10:00
|
||||
* @Describe 崩溃通知复制活动(最终修复版)
|
||||
* 核心修复:适配双视图(普通+大视图),确保通知悬浮显示+复制按钮正常显示,支持重复点击
|
||||
* 适配场景:类库/独立应用,Android 4.1+ 全版本,兼容各厂商机型
|
||||
*/
|
||||
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";
|
||||
/** 按钮状态 Extra 键(传递按钮是否启用) */
|
||||
public static final String EXTRA_BTN_ENABLED = "EXTRA_BTN_ENABLED";
|
||||
/** 复制成功提示文本 */
|
||||
private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板";
|
||||
/** 按钮禁用后自动恢复延迟(1秒,防重复点击) */
|
||||
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
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 初始化全局Handler(仅创建一次,避免重复创建导致任务混乱)
|
||||
mMainHandler = new Handler(Looper.getMainLooper());
|
||||
// 强制透明无界面(无闪屏,用户无感知)
|
||||
setTransparentTheme();
|
||||
// 初始化宿主信息(包名、应用名,确保类库场景意图正确)
|
||||
initHostInfo();
|
||||
// 处理复制按钮点击逻辑(核心)
|
||||
handleCopyAction(getIntent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化宿主应用信息(包名、应用名,类库场景关键)
|
||||
*/
|
||||
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) {
|
||||
// 1. 基础校验(避免非法意图)
|
||||
if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
|
||||
LogUtils.e(TAG, "非复制日志意图,关闭活动");
|
||||
finishAndRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取崩溃日志和按钮状态(从意图中提取,确保数据正确)
|
||||
mCurrentCrashLog = intent.getStringExtra(EXTRA_CRASH_LOG);
|
||||
boolean isBtnEnabled = intent.getBooleanExtra(EXTRA_BTN_ENABLED, true);
|
||||
|
||||
// 3. 崩溃日志空校验(避免空指针)
|
||||
if (mCurrentCrashLog == null || mCurrentCrashLog.isEmpty()) {
|
||||
LogUtils.e(TAG, "崩溃日志为空,无法复制");
|
||||
showTip("复制失败:崩溃日志为空");
|
||||
finishAndRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 按钮状态校验(避免禁用时重复点击)
|
||||
if (!isBtnEnabled) {
|
||||
LogUtils.w(TAG, "复制按钮已禁用,忽略本次点击");
|
||||
finishAndRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 执行复制逻辑(适配全版本剪贴板API)
|
||||
if (copyTextToClipboard(mCurrentCrashLog)) {
|
||||
LogUtils.d(TAG, "崩溃日志复制成功(长度=" + mCurrentCrashLog.length() + "字符)");
|
||||
showTip(COPY_SUCCESS_TIP);
|
||||
// 核心操作1:更新通知按钮为【禁用】状态(仅更新视图,不重复发通知)
|
||||
updateNotificationBtnState(false);
|
||||
// 核心操作2:延迟1秒后恢复按钮【启用】状态(支持重复点击)
|
||||
mMainHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateNotificationBtnState(true);
|
||||
}
|
||||
}, BTN_ENABLE_DELAY);
|
||||
} else {
|
||||
LogUtils.e(TAG, "崩溃日志复制失败");
|
||||
showTip("复制失败,请重试");
|
||||
}
|
||||
|
||||
// 6. 关闭透明活动(用户无感知,释放资源)
|
||||
finishAndRelease();
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心修复:更新通知按钮状态(适配双视图,确保按钮显示+状态切换)
|
||||
* 调用工具类更新 RemoteViews(普通视图+大视图),不重复发送通知
|
||||
* @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) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// 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);
|
||||
clipboard.setPrimaryClip(clipData);
|
||||
} else {
|
||||
// Android 10及以下剪贴板API(适配低版本)
|
||||
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard == null) return false;
|
||||
clipboard.setText(text);
|
||||
}
|
||||
return true;
|
||||
} catch (SecurityException e) {
|
||||
// 类库场景关键:捕获宿主剪贴板权限异常(部分机型限制类库访问)
|
||||
LogUtils.e(TAG, "复制失败:宿主剪贴板权限被拒绝", e);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "复制文本到剪贴板失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示提示(优先使用项目封装的ToastUtils,失败降级系统Toast,确保提示正常显示)
|
||||
* @param tip 提示内容
|
||||
*/
|
||||
private void showTip(String tip) {
|
||||
try {
|
||||
// 优先使用ToastUtils(确保样式统一)
|
||||
if (ToastUtils.class != null && ToastUtils.isInited()) {
|
||||
ToastUtils.show(tip);
|
||||
} else {
|
||||
// 降级使用系统Toast(避免ToastUtils未初始化导致提示失败)
|
||||
Toast.makeText(getApplicationContext(), tip, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "显示提示失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一关闭活动并释放资源(避免内存泄漏,确保资源回收)
|
||||
*/
|
||||
private void finishAndRelease() {
|
||||
// 关闭透明活动
|
||||
finish();
|
||||
// Android 12+ 移除任务栈,避免留在最近任务列表
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
// 清空意图,避免重复处理
|
||||
setIntent(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重复点击(更新意图并重新执行复制逻辑,确保每次点击都有效)
|
||||
*/
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent != null && ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
|
||||
setIntent(intent); // 更新为最新意图(确保获取最新日志和按钮状态)
|
||||
handleCopyAction(intent); // 重新执行复制逻辑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止活动旋转时重建(避免复制逻辑重复执行,提升稳定性)
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
setIntent(null); // 清空意图,避免旋转后重复处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动销毁时释放资源(彻底清理,避免内存泄漏)
|
||||
*/
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
finishAndRelease();
|
||||
// 清空Handler所有任务(避免延迟任务导致内存泄漏)
|
||||
if (mMainHandler != null) {
|
||||
mMainHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,21 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.RemoteViews;
|
||||
import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity;
|
||||
|
||||
import cc.winboll.studio.libappbase.CrashHandler;
|
||||
import cc.winboll.studio.libappbase.GlobalCrashActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.R;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 崩溃通知工具集(优化:2行摘要+按钮防溢出)
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/29 21:12
|
||||
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
|
||||
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
|
||||
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
|
||||
*/
|
||||
public class CrashHandleNotifyUtils {
|
||||
|
||||
@@ -35,304 +38,227 @@ public class CrashHandleNotifyUtils {
|
||||
private static final int API_LEVEL_ANDROID_12 = 31;
|
||||
/** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */
|
||||
private static final int FLAG_IMMUTABLE = 0x00000040;
|
||||
/** 通知布局ID(普通+大视图) */
|
||||
private static final int NOTIFICATION_LAYOUT_NORMAL = R.layout.layout_crash_notification_normal; // 通知栏(2行摘要)
|
||||
private static final int NOTIFICATION_LAYOUT_BIG = R.layout.layout_crash_notification_big; // 悬浮/下拉(完整日志)
|
||||
/** 按钮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);
|
||||
|
||||
/** 通知内容最大行数(控制在3行,超出部分省略) */
|
||||
private static final int NOTIFICATION_MAX_LINES = 3;
|
||||
|
||||
|
||||
/**
|
||||
* 处理未捕获异常(核心:生成2行摘要)
|
||||
* 处理未捕获异常(核心方法,类库入口)
|
||||
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
|
||||
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
|
||||
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity)
|
||||
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
|
||||
*/
|
||||
public static void handleUncaughtException(Application app, Intent intent) {
|
||||
String appName = getAppName(app);
|
||||
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
|
||||
if (app == null || appName == null || errorLog == null) {
|
||||
LogUtils.e(TAG, "发送崩溃通知失败:参数为空");
|
||||
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 crashSummary = getCrash2LineSummary(errorLog);
|
||||
// 发送通知(传入摘要用于通知栏,完整日志用于大视图)
|
||||
sendCrashNotification(app, appName, crashSummary, errorLog, app.getPackageName());
|
||||
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
|
||||
String hostAppName = getHostAppName(hostApp, hostPackageName);
|
||||
|
||||
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity)
|
||||
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心方法:提取崩溃日志的2行摘要(异常类型 + 触发时间)
|
||||
* @param errorLog 完整崩溃日志
|
||||
* @return 2行字符串(第一行:异常类型;第二行:触发时间)
|
||||
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
|
||||
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
|
||||
* @param hostApp 宿主应用的 Application 实例
|
||||
* @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
|
||||
*/
|
||||
private static String getCrash2LineSummary(String errorLog) {
|
||||
if (TextUtils.isEmpty(errorLog)) {
|
||||
return "未知异常\n" + getCurrentTime();
|
||||
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);
|
||||
}
|
||||
|
||||
// 第一行:提取异常类型(如: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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
|
||||
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
|
||||
// 第二行:当前时间(格式:HH:mm:ss)
|
||||
String timeStr = getCurrentTime();
|
||||
|
||||
// 组合为2行摘要(确保仅2行,无多余内容)
|
||||
return errorType + "\n" + timeStr;
|
||||
// 调用核心方法处理
|
||||
handleUncaughtException(hostApp, hostPackageName, errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间(格式:HH:mm:ss)
|
||||
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @return 宿主应用名称(读取失败返回 "未知应用")
|
||||
*/
|
||||
private static String getCurrentTime() {
|
||||
return "触发时间:" + DATE_FORMAT.format(new Date());
|
||||
}
|
||||
|
||||
private static String getAppName(Context context) {
|
||||
private static String getHostAppName(Context hostContext, String hostPackageName) {
|
||||
try {
|
||||
return context.getPackageManager().getApplicationLabel(
|
||||
context.getApplicationInfo()
|
||||
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
|
||||
return hostContext.getPackageManager().getApplicationLabel(
|
||||
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
|
||||
).toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取应用名称失败", e);
|
||||
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e);
|
||||
return "未知应用";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知(优化:通知栏显示2行摘要,大视图显示完整日志)
|
||||
* @param context 上下文
|
||||
* @param title 通知标题
|
||||
* @param crashSummary 2行摘要(通知栏显示)
|
||||
* @param fullErrorLog 完整日志(大视图显示)
|
||||
* @param hostPackageName 宿主包名
|
||||
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
|
||||
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @param hostAppName 宿主应用的名称(用于通知标题)
|
||||
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||
*/
|
||||
private static void sendCrashNotification(Context context, String title, String crashSummary,
|
||||
String fullErrorLog, String hostPackageName) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
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失败");
|
||||
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 适配Android 8.0+通知渠道(高重要性,支持悬浮)
|
||||
// 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createCrashNotifyChannel(notificationManager);
|
||||
createCrashNotifyChannel(hostContext, notificationManager);
|
||||
}
|
||||
|
||||
// 2. 构建双视图(普通视图:2行摘要;大视图:完整日志)
|
||||
RemoteViews remoteViewsNormal = createRemoteViews(context, title, crashSummary, true, NOTIFICATION_LAYOUT_NORMAL);
|
||||
RemoteViews remoteViewsBig = createRemoteViews(context, title, fullErrorLog, true, NOTIFICATION_LAYOUT_BIG);
|
||||
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
|
||||
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
|
||||
if (jumpIntent == null) {
|
||||
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 构建意图
|
||||
PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName);
|
||||
PendingIntent copyPendingIntent = getCopyPendingIntent(context, fullErrorLog, hostPackageName, true);
|
||||
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
|
||||
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
|
||||
|
||||
// 4. 构建通知(优化:确保通知栏按钮完整显示)
|
||||
Notification.Builder builder = new Notification.Builder(context);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 双视图设置(普通视图=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);
|
||||
}
|
||||
// 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建)
|
||||
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
|
||||
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
|
||||
bigTextStyle.bigText(errorLog);
|
||||
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
|
||||
builder.setStyle(bigTextStyle);
|
||||
|
||||
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
|
||||
builder
|
||||
.setSmallIcon(context.getApplicationInfo().icon) // 必需:小图标
|
||||
.setContentIntent(launchPendingIntent)
|
||||
.setAutoCancel(true) // 点击关闭,不常驻(避免长期占用通知栏)
|
||||
.setOngoing(false)
|
||||
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
|
||||
.setSmallIcon(hostContext.getApplicationInfo().icon)
|
||||
.setContentTitle(hostAppName + " 崩溃")
|
||||
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
|
||||
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
|
||||
.setAutoCancel(true) // 点击后自动关闭
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setTicker("应用崩溃:" + getCrash2LineSummary(fullErrorLog).split("\n")[0]); // 滚动提示:仅显示异常类型
|
||||
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||
|
||||
// 5. 构建并发送通知
|
||||
Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
|
||||
? builder.build()
|
||||
: builder.getNotification();
|
||||
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
||||
LogUtils.d(TAG, "崩溃通知发送成功(2行摘要+按钮完整显示)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建RemoteViews(适配双视图)
|
||||
*/
|
||||
private static RemoteViews createRemoteViews(Context context, String title, String content, boolean isBtnEnabled, int layoutId) {
|
||||
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), layoutId);
|
||||
|
||||
// 设置标题/内容(普通视图=2行摘要,大视图=完整日志)
|
||||
remoteViews.setTextViewText(TV_TITLE_ID, title + " 崩溃"); // 标题优化:增加"崩溃"标识
|
||||
remoteViews.setTextViewText(TV_CONTENT_ID, content);
|
||||
|
||||
// 绑定按钮点击意图
|
||||
PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, context.getPackageName(), isBtnEnabled);
|
||||
remoteViews.setOnClickPendingIntent(BTN_COPY_ID, copyPendingIntent);
|
||||
|
||||
// 按钮状态+颜色
|
||||
remoteViews.setBoolean(BTN_COPY_ID, "setEnabled", isBtnEnabled);
|
||||
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);
|
||||
// 适配 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 void createCrashNotifyChannel(NotificationManager notificationManager) {
|
||||
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 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 宿主应用的上下文(显式传入,避免类库上下文依赖)
|
||||
*/
|
||||
private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) {
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName);
|
||||
if (launchIntent == null) {
|
||||
launchIntent = new Intent();
|
||||
}
|
||||
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
flags |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
launchIntent,
|
||||
flags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建复制按钮意图
|
||||
*/
|
||||
public static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName, boolean isEnabled) {
|
||||
Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class);
|
||||
copyIntent.setPackage(hostPackageName);
|
||||
copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG);
|
||||
copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog);
|
||||
copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_BTN_ENABLED, isEnabled);
|
||||
copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
int dynamicRequestCode = 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 |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
dynamicRequestCode,
|
||||
copyIntent,
|
||||
flags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭通知
|
||||
*/
|
||||
public static void cancelCrashNotification(Context context) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(CRASH_NOTIFY_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public static void release(Context context) {
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成");
|
||||
public static void release(Context hostContext) {
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?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>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?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>
|
||||
|
||||
@@ -5,6 +5,4 @@
|
||||
<color name="colorAccent">#FF8DFFA2</color>
|
||||
<color name="colorText">#FFFFFB8D</color>
|
||||
<!-- 通知按钮颜色(启用/禁用) -->
|
||||
<color name="color_btn_enabled">#0066CC</color> <!-- 蓝色:启用状态 -->
|
||||
<color name="color_btn_disabled">#999999</color> <!-- 灰色:禁用状态 -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user