Compare commits
12 Commits
appbase-v1
...
appbase-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| e47a64cc87 | |||
| 65161b1a80 | |||
| aa24bc5e11 | |||
| 1b24fc99ef | |||
| be6b7841ed | |||
| e4dc8109aa | |||
| d0e818056a | |||
| c744289896 | |||
| 375c5c1168 | |||
| 5d2d397113 | |||
| fdba61f30c | |||
| d87172a60d |
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Nov 29 02:40:35 HKT 2025
|
||||
stageCount=3
|
||||
#Sun Nov 30 11:43:55 HKT 2025
|
||||
stageCount=6
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.2
|
||||
publishVersion=15.11.5
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.3
|
||||
baseBetaVersion=15.11.6
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.appbase">
|
||||
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:icon="@drawable/ic_winboll"
|
||||
@@ -11,7 +11,7 @@
|
||||
android:resizeableActivity="true"
|
||||
android:process=":App">
|
||||
|
||||
<activity
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
@@ -29,13 +29,15 @@
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -2,6 +2,7 @@ package cc.winboll.studio.appbase;
|
||||
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.BuildConfig;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -21,6 +22,8 @@ public class App extends GlobalApplication {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
|
||||
//setIsDebugging(false);
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Nov 29 02:40:35 HKT 2025
|
||||
stageCount=3
|
||||
#Sun Nov 30 11:43:55 HKT 2025
|
||||
stageCount=6
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.2
|
||||
publishVersion=15.11.5
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.3
|
||||
baseBetaVersion=15.11.6
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.libappbase">
|
||||
|
||||
<application>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".CrashHandler$CrashActivity"
|
||||
android:label="CrashActivity"
|
||||
@@ -28,6 +29,21 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- 崩溃通知复制动作接收活动(透明无界面) -->
|
||||
<activity
|
||||
android:name="cc.winboll.studio.libappbase.CrashCopyReceiverActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:launchMode="singleTask"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:exported="true">
|
||||
<!-- 注册复制动作的意图过滤器,接收通知按钮点击 -->
|
||||
<intent-filter>
|
||||
<action android:name="cc.winboll.studio.action.COPY_CRASH_LOG" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -185,10 +186,17 @@ public final class CrashHandler {
|
||||
);
|
||||
|
||||
try {
|
||||
// 启动崩溃页面,终止当前进程(确保完全重启)
|
||||
app.startActivity(intent);
|
||||
if (GlobalApplication.isDebugging()) {
|
||||
// 如果是 debug 版,启动崩溃页面窗口
|
||||
app.startActivity(intent);
|
||||
} else {
|
||||
// 如果是 release 版,就只发送一个通知
|
||||
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
|
||||
}
|
||||
// 终止当前进程(确保完全重启)
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -121,6 +121,18 @@ public class ToastUtils {
|
||||
LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
|
||||
}
|
||||
|
||||
// ===================================== 新增:isInited() 方法 =====================================
|
||||
/**
|
||||
* 判断 ToastUtils 是否已初始化(供外部调用,如 CrashHandleNotifyUtils 中的复制提示)
|
||||
* @return true:已初始化(可正常显示吐司);false:未初始化/已释放(无法正常显示)
|
||||
*/
|
||||
public static boolean isInited() {
|
||||
ToastUtils instance = getInstance();
|
||||
// 双重校验:1. 未释放 2. 上下文已设置(确保初始化完成)
|
||||
return !instance.isReleased && instance.mContext != null;
|
||||
}
|
||||
// ===================================== 新增结束 =====================================
|
||||
|
||||
/**
|
||||
* 外部接口:显示短时长吐司
|
||||
* @param message 吐司内容
|
||||
@@ -243,7 +255,6 @@ public class ToastUtils {
|
||||
instance.mWorkerThread.join(1000);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
//LogUtils.e(TAG, "线程退出异常", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
instance.mWorkerThread = null;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package cc.winboll.studio.libappbase.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/01 10:00
|
||||
* @Describe 崩溃通知复制动作接收活动(透明无界面)
|
||||
* 专门接收崩溃通知中“复制日志”按钮的点击事件,处理崩溃日志复制到剪贴板逻辑
|
||||
* 优势:相比广播接收器,活动在应用崩溃后仍能被系统唤醒,确保复制功能稳定生效
|
||||
*/
|
||||
public class CrashCopyReceiverActivity extends Activity {
|
||||
|
||||
/** 日志 TAG(标识当前类日志来源) */
|
||||
public static final String TAG = "CrashCopyReceiverActivity";
|
||||
/** 复制动作 Action(需与 CrashHandleNotifyUtils 中一致) */
|
||||
public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG";
|
||||
/** 崩溃日志 Extra 键(需与 CrashHandleNotifyUtils 中一致) */
|
||||
public static final String EXTRA_CRASH_LOG = "EXTRA_CRASH_LOG";
|
||||
/** 复制成功提示文本 */
|
||||
private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板";
|
||||
/** 活动自动关闭延迟(毫秒):避免占用资源,复制完成后快速关闭 */
|
||||
private static final long AUTO_FINISH_DELAY = 500;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 强制设置活动透明(兼容不同主题配置,避免出现白色界面)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
getWindow().setFlags(
|
||||
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
|
||||
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
);
|
||||
getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||
}
|
||||
|
||||
// 处理复制按钮点击事件
|
||||
handleCopyAction(getIntent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理崩溃日志复制逻辑
|
||||
* @param intent 接收的意图(携带崩溃日志)
|
||||
*/
|
||||
private void handleCopyAction(Intent intent) {
|
||||
if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
|
||||
LogUtils.e(TAG, "非崩溃日志复制意图,直接关闭活动");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// 从意图中获取崩溃日志
|
||||
String crashLog = intent.getStringExtra(EXTRA_CRASH_LOG);
|
||||
if (crashLog == null || crashLog.isEmpty()) {
|
||||
LogUtils.e(TAG, "崩溃日志为空,无法复制");
|
||||
showTip("复制失败:崩溃日志为空");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// 复制日志到剪贴板
|
||||
if (copyTextToClipboard(crashLog)) {
|
||||
LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符");
|
||||
showTip(COPY_SUCCESS_TIP);
|
||||
} else {
|
||||
LogUtils.e(TAG, "崩溃日志复制失败");
|
||||
showTip("复制失败,请重试");
|
||||
}
|
||||
|
||||
// 延迟关闭活动(确保提示能正常显示,用户有感知)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish(); // 关闭透明活动,释放资源
|
||||
// 适配 Android 12+(API 31+):避免活动留在任务栈中(修复 S 常量报错)
|
||||
if (Build.VERSION.SDK_INT >= 31) { // 核心修复:用 31 替代 Build.VERSION_CODES.S
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
}, AUTO_FINISH_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到系统剪贴板(兼容全Android版本)
|
||||
* @param text 需复制的文本(崩溃日志)
|
||||
* @return true:复制成功;false:复制失败
|
||||
*/
|
||||
private boolean copyTextToClipboard(String text) {
|
||||
try {
|
||||
// 适配 Android 11+(API 30+)剪贴板 API
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clipData = android.content.ClipData.newPlainText("崩溃日志", text);
|
||||
clipboard.setPrimaryClip(clipData);
|
||||
} else {
|
||||
// 适配 Android 10 及以下版本剪贴板 API
|
||||
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
clipboard.setText(text);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "复制文本到剪贴板失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示提示信息(优先使用 ToastUtils,失败则降级为系统 Toast)
|
||||
* @param tip 提示内容
|
||||
*/
|
||||
private void showTip(String tip) {
|
||||
try {
|
||||
// 优先使用项目封装的 ToastUtils(确保样式统一)
|
||||
if (ToastUtils.isInited()) {
|
||||
ToastUtils.show(tip);
|
||||
} else {
|
||||
// 降级使用系统 Toast(确保提示能正常显示)
|
||||
Toast.makeText(this, tip, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "显示提示失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重复意图(避免多次触发复制)
|
||||
*/
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent); // 更新当前意图
|
||||
handleCopyAction(intent); // 重新处理复制动作
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止活动旋转时重建(避免复制逻辑重复执行)
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package cc.winboll.studio.libappbase.utils;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity;
|
||||
import cc.winboll.studio.libappbase.CrashHandler;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.R;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/29 21:12
|
||||
* @Describe 应用崩溃处理通知实用工具集(类库专用,支持宿主应用唤醒活动)
|
||||
* 核心功能:应用崩溃时捕获错误日志,发送通知到系统通知栏(3行内容省略+复制按钮),点击复制按钮唤醒CrashCopyReceiverActivity完成日志拷贝
|
||||
*/
|
||||
public class CrashHandleNotifyUtils {
|
||||
|
||||
public static final String TAG = "CrashHandleNotifyUtils";
|
||||
|
||||
/** 通知渠道ID(Android 8.0+ 必须,用于归类通知) */
|
||||
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
|
||||
/** 通知渠道名称(用户可见,描述渠道用途) */
|
||||
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
|
||||
/** 通知ID(唯一标识一条通知,避免重复创建) */
|
||||
private static final int CRASH_NOTIFY_ID = 0x001;
|
||||
/** Android 12 对应 API 版本号(31),替代 Build.VERSION_CODES.S */
|
||||
private static final int API_LEVEL_ANDROID_12 = 31;
|
||||
/** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+),避免依赖高版本 SDK */
|
||||
private static final int FLAG_IMMUTABLE = 0x00000040;
|
||||
/** 通知内容最大行数(控制在3行,超出部分省略) */
|
||||
private static final int NOTIFICATION_MAX_LINES = 3;
|
||||
/** 复制按钮请求码(区分多个 PendingIntent) */
|
||||
private static final int REQUEST_CODE_COPY = 0x002;
|
||||
|
||||
/**
|
||||
* 处理未捕获异常(核心方法,类库场景专用:强制使用宿主应用包名构建意图)
|
||||
* 1. 提取应用名称和崩溃日志;
|
||||
* 2. 创建并发送系统通知(3行内容省略+复制按钮);
|
||||
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
|
||||
* @param app 应用全局 Application 实例(宿主应用的 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 + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发送崩溃通知到通知栏(类库场景:强制用宿主包名构建意图)
|
||||
sendCrashNotification(app, appName, errorLog, app.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用真实名称(从宿主 AndroidManifest 中读取 android:label)
|
||||
* @param context 上下文(宿主 Application 实例,确保获取正确的应用名称)
|
||||
* @return 应用名称(读取失败返回 "未知应用")
|
||||
*/
|
||||
private static String getAppName(Context context) {
|
||||
try {
|
||||
// 从宿主包管理器中获取应用信息(类库场景必须用宿主上下文)
|
||||
return context.getPackageManager().getApplicationLabel(
|
||||
context.getApplicationInfo()
|
||||
).toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取应用名称失败", e);
|
||||
return "未知应用";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送崩溃通知到系统通知栏(类库专用:新增宿主包名参数,确保意图跳转正确)
|
||||
* @param context 上下文(宿主 Application 实例)
|
||||
* @param title 通知标题(应用名称)
|
||||
* @param content 通知内容(崩溃日志)
|
||||
* @param hostPackageName 宿主应用包名(关键:用于构建跨类库的活动意图)
|
||||
*/
|
||||
private static void sendCrashNotification(Context context, String title, String content, String hostPackageName) {
|
||||
// 1. 获取通知管理器(系统服务,用于发送/管理通知)
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager == null) {
|
||||
LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 适配 Android 8.0+(API 26+):创建通知渠道(必须,否则通知不显示)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createCrashNotifyChannel(notificationManager);
|
||||
}
|
||||
|
||||
// 3. 构建通知意图(类库场景:用宿主包名构建意图,确保跳转成功)
|
||||
PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); // 主界面跳转意图
|
||||
PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, hostPackageName); // 唤醒复制活动的意图
|
||||
|
||||
// 4. 构建通知实例(核心修复:3行内容省略+复制按钮)
|
||||
Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent);
|
||||
|
||||
// 5. 发送通知(指定通知ID,重复发送同ID会覆盖原通知)
|
||||
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
||||
LogUtils.d(TAG, "崩溃通知发送成功(类库场景):标题=" + title + ",宿主包名=" + hostPackageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建崩溃通知渠道(Android 8.0+ 必需)
|
||||
* @param notificationManager 通知管理器
|
||||
*/
|
||||
private static void createCrashNotifyChannel(NotificationManager notificationManager) {
|
||||
// 仅 Android 8.0+ 执行(避免低版本报错)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// 构建通知渠道(指定ID、名称、重要性)
|
||||
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
||||
CRASH_NOTIFY_CHANNEL_ID,
|
||||
CRASH_NOTIFY_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
|
||||
);
|
||||
// 可选:设置渠道描述(用户在设置中可见)
|
||||
channel.setDescription("用于显示应用崩溃信息,支持复制日志");
|
||||
// 注册通知渠道到系统
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通知点击跳转意图(跳转宿主应用主界面,类库场景专用)
|
||||
* @param context 上下文(宿主 Application 实例)
|
||||
* @param hostPackageName 宿主应用包名(关键:确保跳转宿主主界面)
|
||||
* @return 主界面跳转 PendingIntent
|
||||
*/
|
||||
private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) {
|
||||
// 1. 获取宿主应用主界面 Intent(强制用宿主包名获取,避免类库包名干扰)
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName);
|
||||
if (launchIntent == null) {
|
||||
// 异常处理:若主界面 Intent 为空,创建空意图(避免崩溃)
|
||||
launchIntent = new Intent();
|
||||
}
|
||||
|
||||
// 2. 构建 PendingIntent(延迟执行的意图,类库场景增加 FLAG_ONE_SHOT 确保单次有效)
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
// 适配 Android 12+(API 31+):添加 FLAG_IMMUTABLE 避免安全警告
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
flags |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0, // 请求码(可忽略)
|
||||
launchIntent,
|
||||
flags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建复制按钮意图(类库场景专用:用宿主包名唤醒活动,确保跳转成功)
|
||||
* @param context 上下文(宿主 Application 实例)
|
||||
* @param errorLog 崩溃日志(需要复制的内容)
|
||||
* @param hostPackageName 宿主应用包名(关键:确保系统能找到类库中的活动)
|
||||
* @return 唤醒复制活动的 PendingIntent
|
||||
*/
|
||||
private static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName) {
|
||||
// 1. 构建唤醒 CrashCopyReceiverActivity 的显式意图(类库场景关键:指定宿主包名)
|
||||
Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class);
|
||||
// 强制设置意图的包名为宿主包名(解决类库与宿主包名不匹配问题)
|
||||
copyIntent.setPackage(hostPackageName);
|
||||
// 设置动作(与Activity中匹配,确保意图精准匹配)
|
||||
copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG);
|
||||
// 携带完整崩溃日志(键与Activity中一致)
|
||||
copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog);
|
||||
// 类库场景增强标志:确保活动在宿主应用中能被唤醒,且不重复创建
|
||||
copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
| Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
| Intent.FLAG_ACTIVITY_NO_HISTORY); // 不保留活动历史,避免残留
|
||||
|
||||
// 2. 构建 PendingIntent(类库场景:使用 FLAG_ONE_SHOT 避免重复触发)
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT;
|
||||
// 适配 Android 12+(API 31+):添加 FLAG_IMMUTABLE 避免安全警告
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
flags |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
// 3. 返回活动类型的 PendingIntent(类库场景:用宿主上下文构建,确保权限通过)
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
REQUEST_CODE_COPY, // 唯一请求码,区分主界面意图
|
||||
copyIntent,
|
||||
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) {
|
||||
// 兼容 Android 8.0+:指定通知渠道ID
|
||||
Notification.Builder builder = new Notification.Builder(context);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
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) {
|
||||
// 因已移除广播接收器,此处仅保留空实现,避免调用方报错
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(类库场景,无广播接收器需注销)");
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user