diff --git a/appbase/build.properties b/appbase/build.properties
index f956dcc..c1f31c4 100644
--- a/appbase/build.properties
+++ b/appbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Sat Dec 06 14:24:56 HKT 2025
+#Sat Dec 06 06:54:32 GMT 2025
stageCount=2
libraryProject=libappbase
baseVersion=15.12
publishVersion=15.12.1
-buildCount=0
+buildCount=2
baseBetaVersion=15.12.2
diff --git a/appbase/src/main/AndroidManifest.xml b/appbase/src/main/AndroidManifest.xml
index 284d7af..2b21bf8 100644
--- a/appbase/src/main/AndroidManifest.xml
+++ b/appbase/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
-
+
-
-
+
-
+
+
+
-
+
\ No newline at end of file
diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/App.java b/appbase/src/main/java/cc/winboll/studio/appbase/App.java
index 888d581..b834044 100644
--- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java
+++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java
@@ -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
@@ -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());
}
diff --git a/libappbase/build.properties b/libappbase/build.properties
index 6dc9053..c1f31c4 100644
--- a/libappbase/build.properties
+++ b/libappbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Sat Dec 06 14:24:33 HKT 2025
+#Sat Dec 06 06:54:32 GMT 2025
stageCount=2
libraryProject=libappbase
baseVersion=15.12
publishVersion=15.12.1
-buildCount=0
+buildCount=2
baseBetaVersion=15.12.2
diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml
index 61a602b..857b928 100644
--- a/libappbase/src/main/AndroidManifest.xml
+++ b/libappbase/src/main/AndroidManifest.xml
@@ -3,8 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
-
+
+
- * @Date 2025/11/11 20:36
- * @Describe WinBoLl 应用日志管理工具类(单例逻辑)
- * 核心功能:日志分级控制、日志文件读写、TAG 过滤配置、应用内所有 TAG 自动扫描
- * 支持 Debug/Release 模式区分存储路径,日志持久化与清理
- * 适配 Java 7 语法,移除 Lambda、Stream 等 Java 8+ 特性
- */
+
public class LogUtils {
- /** 当前工具类的日志 TAG */
public static final String TAG = "LogUtils";
- /**
- * 日志级别枚举(从低到高:关闭→错误→警告→信息→调试→详细)
- * 级别越高,输出的日志越详细
- */
public static enum LOG_LEVEL { Off, Error, Warn, Info, Debug, Verbose }
- /** 是否初始化完成标志(volatile 保证多线程可见性) */
- private static volatile boolean sIsInited = false;
- /** 全局上下文(用于获取存储路径、包信息) */
- private static Context sContext;
- /** 日志时间格式化工具(格式:[yyyyMMdd_HHmmss_SSS],精确到毫秒) */
- private static SimpleDateFormat sSimpleDateFormat = new SimpleDateFormat("[yyyyMMdd_HHmmss_SSS]", Locale.getDefault());
- /** 日志缓存文件夹(Debug 模式下存储在外部缓存,Release 存储在内部缓存) */
- private static File sLogCacheDir;
- /** 日志配置文件夹(存储 TAG 配置文件) */
- private static File sLogDataDir;
- /** 日志存储文件(所有日志写入此文件) */
- private static File sLogFile;
- /** 日志配置文件(存储日志级别、TAG 启用状态等配置) */
- private static File sLogConfigFile;
- /** 日志配置实体类(封装日志级别等配置) */
- private static LogUtilsBean sLogConfigBean;
- /** TAG 过滤映射表(key:TAG 名称;value:是否启用该 TAG 的日志输出) */
- public static Map sTagEnableMap = new HashMap();
+ static volatile boolean _IsInited = false;
+ static Context _mContext;
+ // 日志显示时间格式
+ static SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("[yyyyMMdd_HHmmss_SSS]", Locale.getDefault());
+ // 应用日志文件夹
+ static File _mfLogCacheDir;
+ static File _mfLogDataDir;
+ // 应用日志文件
+ static File _mfLogCatchFile;
+ static File _mfLogUtilsBeanFile;
+ static LogUtilsBean _mLogUtilsBean;
+ public static Map mapTAGList = new HashMap();
- /**
- * 初始化日志工具(默认日志级别:Off,不输出日志)
- * @param context 全局上下文(建议传入 Application 实例)
- */
+ //
+ // 初始化函数
+ //
public static void init(Context context) {
- sContext = context;
+ _mContext = context;
init(context, LOG_LEVEL.Off);
}
- /**
- * 初始化日志工具(指定日志级别)
- * 1. 根据 Debug/Release 模式初始化日志存储路径;
- * 2. 加载日志配置文件;
- * 3. 扫描应用内所有类的 TAG 并初始化过滤映射表;
- * 4. 标记初始化完成。
- * @param context 全局上下文
- * @param logLevel 初始日志级别
- */
+ //
+ // 初始化函数
+ //
public static void init(Context context, LOG_LEVEL logLevel) {
- sContext = context;
- // 根据 Debug 模式选择存储路径(外部/内部存储)
if (GlobalApplication.isDebugging()) {
- // Debug 模式:存储在外部缓存目录(可通过文件管理器查看)
- sLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
- sLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
+ // 初始化日志缓存文件路径(debug模式:外部存储)
+ _mfLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
+ if (!_mfLogCacheDir.exists()) {
+ _mfLogCacheDir.mkdirs();
+ }
+ _mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
+
+ // 初始化日志配置文件路径
+ _mfLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
+ if (!_mfLogDataDir.exists()) {
+ _mfLogDataDir.mkdirs();
+ }
+ _mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
} else {
- // Release 模式:存储在内部缓存目录(仅应用自身可访问)
- sLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
- sLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
+ // 初始化日志缓存文件路径(release模式:内部存储)
+ _mfLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
+ if (!_mfLogCacheDir.exists()) {
+ _mfLogCacheDir.mkdirs();
+ }
+ _mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
+
+ // 初始化日志配置文件路径
+ _mfLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
+ if (!_mfLogDataDir.exists()) {
+ _mfLogDataDir.mkdirs();
+ }
+ _mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
}
- // 创建日志文件夹(不存在则创建)
- createDirIfNotExists(sLogCacheDir);
- createDirIfNotExists(sLogDataDir);
-
- // 初始化日志文件和配置文件路径
- sLogFile = new File(sLogCacheDir, "log.txt");
- sLogConfigFile = new File(sLogDataDir, TAG + ".json");
-
- // 加载日志配置(从文件读取,读取失败则创建默认配置)
- sLogConfigBean = LogUtilsBean.loadBeanFromFile(sLogConfigFile.getPath(), LogUtilsBean.class);
- if (sLogConfigBean == null) {
- sLogConfigBean = new LogUtilsBean();
- sLogConfigBean.setLogLevel(logLevel);
- // 保存默认配置到文件
- sLogConfigBean.saveBeanToFile(sLogConfigFile.getPath(), sLogConfigBean);
+ _mLogUtilsBean = LogUtilsBean.loadBeanFromFile(_mfLogUtilsBeanFile.getPath(), LogUtilsBean.class);
+ if (_mLogUtilsBean == null) {
+ _mLogUtilsBean = new LogUtilsBean();
+ _mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
}
- // 扫描应用内所有类的 TAG 并添加到过滤映射表
- scanAllClassTags();
- // 加载已保存的 TAG 启用状态配置
- loadTagEnableSettings();
- // 标记初始化完成
- sIsInited = true;
-
- // 打印初始化日志(调试用)
- d(TAG, String.format("TAG 过滤映射表初始化完成:%s", sTagEnableMap.toString()));
+ // 加载当前应用下的所有类的 TAG
+ addClassTAGList();
+ loadTAGBeanSettings();
+ _IsInited = true;
+ LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
}
- /**
- * 获取 TAG 过滤映射表(外部可通过此方法获取所有 TAG 及其启用状态)
- * @return TAG 名称与启用状态的映射
- */
- public static Map getTagEnableMap() {
- return sTagEnableMap;
+ public static Map getMapTAGList() {
+ return mapTAGList;
}
- /**
- * 加载已保存的 TAG 启用状态配置
- * 从 LogUtilsClassTAGBean 列表中读取每个 TAG 的启用状态,更新到映射表
- */
- private static void loadTagEnableSettings() {
- ArrayList tagSettingList = new ArrayList();
- // 从文件加载 TAG 配置列表
- LogUtilsClassTAGBean.loadBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class);
-
- // 遍历配置列表,更新 TAG 启用状态(Java 7 增强 for 循环)
- for (LogUtilsClassTAGBean tagSetting : tagSettingList) {
- String tag = tagSetting.getTag();
- boolean isEnable = tagSetting.getEnable();
- // 仅更新已存在的 TAG(避免无效配置)
- if (sTagEnableMap.containsKey(tag)) {
- sTagEnableMap.put(tag, isEnable);
+ static void loadTAGBeanSettings() {
+ ArrayList list = new ArrayList();
+ LogUtilsClassTAGBean.loadBeanList(_mContext, list, LogUtilsClassTAGBean.class);
+ for (int i = 0; i < list.size(); i++) {
+ LogUtilsClassTAGBean beanSetting = list.get(i);
+ for (Map.Entry entry : mapTAGList.entrySet()) {
+ if (entry.getKey().equals(beanSetting.getTag())) {
+ entry.setValue(beanSetting.getEnable());
+ }
}
}
}
- /**
- * 保存当前 TAG 启用状态配置到文件
- * 将映射表中的 TAG 及其启用状态转换为 LogUtilsClassTAGBean 列表,持久化到文件
- */
- private static void saveTagEnableSettings() {
- ArrayList tagSettingList = new ArrayList();
- // 遍历映射表,构建配置列表(Java 7 迭代器遍历)
- Iterator> iterator = sTagEnableMap.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry entry = iterator.next();
- tagSettingList.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
+ static void saveTAGBeanSettings() {
+ ArrayList list = new ArrayList();
+ for (Map.Entry entry : mapTAGList.entrySet()) {
+ list.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
}
- // 保存配置列表到文件
- LogUtilsClassTAGBean.saveBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class);
+ LogUtilsClassTAGBean.saveBeanList(_mContext, list, LogUtilsClassTAGBean.class);
}
- /**
- * 扫描应用内所有类的 TAG 并添加到过滤映射表
- * 1. 通过 DexFile 读取 APK 中所有类;
- * 2. 过滤指定包名前缀(cc.winboll.studio)的类;
- * 3. 反射获取类中 public static final String TAG 字段的值;
- * 4. 将 TAG 加入映射表,默认禁用(false)。
- */
- private static void scanAllClassTags() {
+ static void addClassTAGList() {
try {
- // 应用 APK 路径(通过上下文获取)
- String apkPath = sContext.getPackageCodePath();
- d(TAG, String.format("APK 路径:%s", apkPath));
+ // 包名前缀(过滤当前应用的类)
+ String packageNamePrefix = "cc.winboll.studio";
+ List classNames = new ArrayList<>();
+ String apkPath = _mContext.getPackageCodePath();
+ LogUtils.d(TAG, String.format("apkPath : %s", apkPath));
- // 读取 APK 中的所有类
- DexFile dexFile = new DexFile(apkPath);
- Enumeration classNames = dexFile.entries();
-
- int totalClassCount = 0; // 总类数(调试用)
- List targetClassNames = new ArrayList(); // 目标包名下的类名列表
- String targetPackagePrefix = "cc.winboll.studio"; // 目标包名前缀
-
- // 过滤目标包名下的类(Java 7 枚举遍历)
- while (classNames.hasMoreElements()) {
- totalClassCount++;
- String className = classNames.nextElement();
- if (className.startsWith(targetPackagePrefix)) {
- targetClassNames.add(className);
+ DexFile dexfile = new DexFile(apkPath);
+ int countTemp = 0;
+ Enumeration entries = dexfile.entries();
+ while (entries.hasMoreElements()) {
+ countTemp++;
+ String className = entries.nextElement();
+ if (className.startsWith(packageNamePrefix)) {
+ classNames.add(className);
}
}
- // 打印扫描统计(调试用)
- d(TAG, String.format("APK 总类数:%d,目标包下类数:%d", totalClassCount, targetClassNames.size()));
+ LogUtils.d(TAG, String.format("countTemp : %d\nClassNames size : %d", countTemp, classNames.size()));
- // 反射获取每个类的 TAG 字段(Java 7 增强 for 循环)
- for (String className : targetClassNames) {
+ for (String className : classNames) {
try {
Class> clazz = Class.forName(className);
- // 获取类中所有声明的字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
- // 过滤条件:public static String 类型,且字段名是 "TAG"
- if (Modifier.isStatic(field.getModifiers())
- && Modifier.isPublic(field.getModifiers())
- && field.getType() == String.class
+ // 过滤静态、公共、String类型的 TAG 字段
+ if (Modifier.isStatic(field.getModifiers())
+ && Modifier.isPublic(field.getModifiers())
+ && field.getType() == String.class
&& "TAG".equals(field.getName())) {
- // 获取 TAG 字段的值(静态字段,传入 null 即可)
String tagValue = (String) field.get(null);
- // 添加到映射表,默认禁用
- sTagEnableMap.put(tagValue, false);
+ mapTAGList.put(tagValue, false); // 默认禁用,可通过设置开启
}
}
- } catch (NoClassDefFoundError e) {
- // 捕获反射异常,避免单个类扫描失败影响整体
- d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (ClassNotFoundException e) {
- d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ } catch (NoClassDefFoundError | ClassNotFoundException | IllegalAccessException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
}
}
} catch (IOException e) {
- // 捕获 APK 读取异常
- d(TAG, e, Thread.currentThread().getStackTrace());
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
- /**
- * 设置单个 TAG 的启用状态
- * @param tag TAG 名称
- * @param isEnable 是否启用(true:输出该 TAG 的日志;false:不输出)
- */
- public static void setTagEnable(String tag, boolean isEnable) {
- // 遍历映射表,更新目标 TAG 的状态(Java 7 迭代器遍历)
- Iterator> iterator = sTagEnableMap.entrySet().iterator();
+ public static void setTAGListEnable(String tag, boolean isEnable) {
+ Iterator> iterator = mapTAGList.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
if (tag.equals(entry.getKey())) {
@@ -243,395 +182,551 @@ public class LogUtils {
break;
}
}
- // 保存配置到文件(持久化)
- saveTagEnableSettings();
- d(TAG, String.format("TAG 配置更新:%s", sTagEnableMap.toString()));
+ saveTAGBeanSettings();
+ LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
}
- /**
- * 设置所有 TAG 的启用状态(批量控制)
- * @param isEnable 是否启用(true:所有 TAG 均输出日志;false:所有 TAG 均不输出)
- */
- public static void setAllTagsEnable(boolean isEnable) {
- // 遍历映射表,批量更新所有 TAG 的状态(Java 7 迭代器遍历)
- Iterator> iterator = sTagEnableMap.entrySet().iterator();
+ public static void setALlTAGListEnable(boolean isEnable) {
+ Iterator> iterator = mapTAGList.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
entry.setValue(isEnable);
}
- // 保存配置到文件(持久化)
- saveTagEnableSettings();
- d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableMap.toString()));
+ saveTAGBeanSettings();
+ LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
}
- /**
- * 设置全局日志级别(控制日志输出的详细程度)
- * @param logLevel 目标日志级别
- */
public static void setLogLevel(LOG_LEVEL logLevel) {
- if (sLogConfigBean != null) {
- sLogConfigBean.setLogLevel(logLevel);
- // 保存配置到文件(持久化)
- sLogConfigBean.saveBeanToFile(sLogConfigFile.getPath(), sLogConfigBean);
- }
+ LogUtils._mLogUtilsBean.setLogLevel(logLevel);
+ _mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
}
- /**
- * 获取当前全局日志级别
- * @return 当前日志级别
- */
public static LOG_LEVEL getLogLevel() {
- return sLogConfigBean != null ? sLogConfigBean.getLogLevel() : LOG_LEVEL.Off;
+ return LogUtils._mLogUtilsBean.getLogLevel();
}
- /**
- * 判断当前日志是否可输出(校验初始化状态、TAG 启用状态、日志级别)
- * @param tag 日志 TAG
- * @param logLevel 日志级别
- * @return true:可输出;false:不可输出
- */
- private static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
- // 未初始化:不输出
- if (!sIsInited) {
+ static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
+ if (!_IsInited) {
return false;
- }
- // TAG 未配置或未启用:不输出
- if (sTagEnableMap.get(tag) == null || !sTagEnableMap.get(tag)) {
+ }
+ // TAG 未配置或禁用时,不打印日志
+ if (mapTAGList.get(tag) == null || !mapTAGList.get(tag)) {
return false;
- }
- // 日志级别未达到:不输出
- if (!isLevelMatched(logLevel)) {
+ }
+ // 日志级别不符合时,不打印日志
+ if (!isInTheLevel(logLevel)) {
return false;
}
return true;
}
- /**
- * 判断日志级别是否匹配(当前全局级别 >= 目标级别时可输出)
- * 例:全局级别为 Debug(4),则 Error(1)、Warn(2)、Info(3)、Debug(4)均可输出
- * @param logLevel 目标日志级别
- * @return true:级别匹配;false:不匹配
- */
- private static boolean isLevelMatched(LOG_LEVEL logLevel) {
- if (sLogConfigBean == null) {
- return false;
- }
- // 枚举的 ordinal() 方法返回索引(Off=0,Error=1,...,Verbose=5)
- return sLogConfigBean.getLogLevel().ordinal() >= logLevel.ordinal();
+ static boolean isInTheLevel(LOG_LEVEL logLevel) {
+ // 当前日志级别 >= 目标级别时,允许打印(级别顺序:Off < Error < Warn < Info < Debug < Verbose)
+ return LogUtils._mLogUtilsBean.getLogLevel().ordinal() >= logLevel.ordinal();
}
- /**
- * 获取日志缓存文件夹路径(外部可通过此方法获取日志存储目录)
- * @return 日志缓存文件夹
- */
+ //
+ // 获取应用日志文件夹
+ //
public static File getLogCacheDir() {
- return sLogCacheDir;
+ return _mfLogCacheDir;
}
+ // ================================= 补全所有日志重载方法(Error 级别) =================================
/**
- * 输出 Error 级别日志
- * @param tag TAG 名称
- * @param message 日志内容
+ * Error 级别日志(仅消息)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
*/
- public static void e(String tag, String message) {
- if (isLoggable(tag, LOG_LEVEL.Error)) {
- saveLog(tag, LOG_LEVEL.Error, message);
+ public static void e(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Error, szMessage);
}
}
/**
- * 输出 Error 级别日志(带异常信息和调用栈)
- * 错误级别专用,包含完整异常详情,便于错误定位和排查
- * @param tag TAG 名称
- * @param message 日志内容
- * @param e 异常对象(存储异常信息和调用栈)
- */
- public static void e(String tag, String message, Exception e) {
- if (isLoggable(tag, LOG_LEVEL.Error)) {
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- StringBuilder sb = new StringBuilder(message);
- // 拼接异常信息(类型 + 消息)
- sb.append(" \nException: ")
- .append(e.getClass().getSimpleName())
- .append(" : ")
- .append(e.getMessage() != null ? e.getMessage() : "无异常消息");
- // 拼接调用栈信息(stackTrace[2] 为实际调用处)
- sb.append(" \nAt ")
- .append(stackTrace[2].getMethodName())
- .append(" (")
- .append(stackTrace[2].getFileName())
- .append(":")
- .append(stackTrace[2].getLineNumber())
- .append(")");
- saveLog(tag, LOG_LEVEL.Error, sb.toString());
- }
- }
-
- /**
- * 输出 Warn 级别日志
- * @param tag TAG 名称
- * @param message 日志内容
- */
- public static void w(String tag, String message) {
- if (isLoggable(tag, LOG_LEVEL.Warn)) {
- saveLog(tag, LOG_LEVEL.Warn, message);
- }
- }
-
- /**
- * 输出 Warn 级别日志(带异常信息和调用栈)
- * 包含日志内容、异常详情、调用位置,便于警告场景下的问题定位
- * @param tag TAG 名称
- * @param message 日志内容
- * @param e 异常对象(存储异常信息和调用栈)
- */
- public static void w(String tag, String message, Exception e) {
- if (isLoggable(tag, LOG_LEVEL.Warn)) {
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- StringBuilder sb = new StringBuilder(message);
- // 拼接异常信息(类型 + 消息)
- sb.append(" \nException: ")
- .append(e.getClass().getSimpleName())
- .append(" : ")
- .append(e.getMessage() != null ? e.getMessage() : "无异常消息");
- // 拼接调用栈信息(stackTrace[2] 为实际调用处)
- sb.append(" \nAt ")
- .append(stackTrace[2].getMethodName())
- .append(" (")
- .append(stackTrace[2].getFileName())
- .append(":")
- .append(stackTrace[2].getLineNumber())
- .append(")");
- saveLog(tag, LOG_LEVEL.Warn, sb.toString());
- }
- }
-
- /**
- * 输出 Info 级别日志
- * @param tag TAG 名称
- * @param message 日志内容
- */
- public static void i(String tag, String message) {
- if (isLoggable(tag, LOG_LEVEL.Info)) {
- saveLog(tag, LOG_LEVEL.Info, message);
- }
- }
-
- /**
- * 输出 Debug 级别日志(基础版)
- * @param tag TAG 名称
- * @param message 日志内容
- */
- public static void d(String tag, String message) {
- if (isLoggable(tag, LOG_LEVEL.Debug)) {
- saveLog(tag, LOG_LEVEL.Debug, message);
- }
- }
-
- /**
- * 输出 Debug 级别日志(带调用栈信息)
- * 包含调用方法名、文件名、行号,便于调试定位
- * @param tag TAG 名称
- * @param message 日志内容
- * @param stackTrace 线程调用栈(通常传入 Thread.currentThread().getStackTrace())
- */
- public static void d(String tag, String message, StackTraceElement[] stackTrace) {
- if (isLoggable(tag, LOG_LEVEL.Debug)) {
- StringBuilder sb = new StringBuilder(message);
- // 拼接调用栈信息(stackTrace[2] 为实际调用处)
- sb.append(" \nAt ")
- .append(stackTrace[2].getMethodName())
- .append(" (")
- .append(stackTrace[2].getFileName())
- .append(":")
- .append(stackTrace[2].getLineNumber())
- .append(")");
- saveLog(tag, LOG_LEVEL.Debug, sb.toString());
- }
- }
-
- /**
- * 输出 Debug 级别日志(带异常信息和调用栈)
- * 包含异常类型、异常信息、调用位置,便于异常定位
- * @param tag TAG 名称
+ * Error 级别日志(消息 + 异常)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
* @param e 异常对象
- * @param stackTrace 线程调用栈
*/
- public static void d(String tag, Exception e, StackTraceElement[] stackTrace) {
- if (isLoggable(tag, LOG_LEVEL.Debug)) {
- StringBuilder sb = new StringBuilder();
- // 拼接异常信息
- sb.append(e.getClass().getSimpleName())
- .append(" : ")
- .append(e.getMessage() != null ? e.getMessage() : "无异常消息")
- // 拼接调用栈信息
- .append(" \nAt ")
- .append(stackTrace[2].getMethodName())
- .append(" (")
- .append(stackTrace[2].getFileName())
- .append(":")
- .append(stackTrace[2].getLineNumber())
- .append(")");
- saveLog(tag, LOG_LEVEL.Debug, sb.toString());
+ public static void e(String szTAG, String szMessage, Exception e) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString());
}
}
/**
- * 输出 Debug 级别日志(带日志内容+异常对象,简化调用)
- * 无需手动传入调用栈,内部自动获取,适配常见调试场景
- * @param tag TAG 名称
- * @param message 日志内容
- * @param e 异常对象(存储异常信息和调用栈)
+ * Error 级别日志(仅异常)
+ * @param szTAG 标签
+ * @param e 异常对象
*/
- public static void d(String tag, String message, Exception e) {
- if (isLoggable(tag, LOG_LEVEL.Debug)) {
- // 自动获取当前线程调用栈,简化外部调用
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- StringBuilder sb = new StringBuilder(message);
- // 拼接异常信息(类型 + 消息)
- sb.append(" \nException: ")
- .append(e.getClass().getSimpleName())
- .append(" : ")
- .append(e.getMessage() != null ? e.getMessage() : "无异常消息");
- // 拼接调用栈信息(stackTrace[2] 为实际调用处)
- sb.append(" \nAt ")
- .append(stackTrace[2].getMethodName())
- .append(" (")
- .append(stackTrace[2].getFileName())
- .append(":")
- .append(stackTrace[2].getLineNumber())
- .append(")");
- saveLog(tag, LOG_LEVEL.Debug, sb.toString());
+ public static void e(String szTAG, Exception e) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
+ String message = "【异常信息】: " + getExceptionInfo(e);
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Error, message);
}
}
/**
- * 输出 Verbose 级别日志(最详细级别)
- * @param tag TAG 名称
- * @param message 日志内容
+ * Error 级别日志(消息 + 异常 + 堆栈)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param e 异常对象
+ * @param listStackTrace 堆栈信息
*/
- public static void v(String tag, String message) {
- if (isLoggable(tag, LOG_LEVEL.Verbose)) {
- saveLog(tag, LOG_LEVEL.Verbose, message);
+ public static void e(String szTAG, String szMessage, Exception e, StackTraceElement[] listStackTrace) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
+ sb.append("\n【堆栈信息】: ").append(getStackTraceInfo(listStackTrace));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString());
+ }
+ }
+
+ // ================================= 补全所有日志重载方法(Warn 级别) =================================
+ /**
+ * Warn 级别日志(仅消息)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ */
+ public static void w(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, szMessage);
}
}
/**
- * 核心日志保存方法(将日志写入文件)
- * 日志格式:[级别] [时间戳] [TAG]
- * 日志内容
- * @param tag TAG 名称
+ * Warn 级别日志(消息 + 异常)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param e 异常对象
+ */
+ public static void w(String szTAG, String szMessage, Exception e) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, sb.toString());
+ }
+ }
+
+ /**
+ * Warn 级别日志(仅异常)
+ * @param szTAG 标签
+ * @param e 异常对象
+ */
+ public static void w(String szTAG, Exception e) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
+ String message = "【异常信息】: " + getExceptionInfo(e);
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, message);
+ }
+ }
+
+ // ================================= 补全所有日志重载方法(Info 级别) =================================
+ /**
+ * Info 级别日志(仅消息)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ */
+ public static void i(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Info, szMessage);
+ }
+ }
+
+ /**
+ * Info 级别日志(消息 + 数据对象)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param obj 数据对象(自动转为字符串)
+ */
+ public static void i(String szTAG, String szMessage, Object obj) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
+ String objStr = obj == null ? "null" : obj.toString();
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【数据对象】: ").append(objStr);
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Info, sb.toString());
+ }
+ }
+
+ // ================================= 补全所有日志重载方法(Debug 级别) =================================
+ /**
+ * Debug 级别日志(仅消息)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ */
+ public static void d(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, szMessage);
+ }
+ }
+
+ /**
+ * Debug 级别日志(消息 + 堆栈)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param listStackTrace 堆栈信息
+ */
+ public static void d(String szTAG, String szMessage, StackTraceElement[] listStackTrace) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ StringBuilder sbMessage = new StringBuilder(szMessage);
+ sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
+ }
+ }
+
+ /**
+ * Debug 级别日志(消息 + 异常)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param e 异常对象
+ */
+ public static void d(String szTAG, String szMessage, Exception e) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【异常信息】: ").append(getExceptionInfo(e));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
+ }
+ }
+
+ /**
+ * Debug 级别日志(异常 + 堆栈)
+ * @param szTAG 标签
+ * @param e 异常对象
+ * @param listStackTrace 堆栈信息
+ */
+ public static void d(String szTAG, Exception e, StackTraceElement[] listStackTrace) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ StringBuilder sbMessage = new StringBuilder();
+ sbMessage.append("【异常信息】: ").append(getExceptionInfo(e));
+ sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace));
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
+ }
+ }
+
+ /**
+ * Debug 级别日志(消息 + 数据对象)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param obj 数据对象(自动转为字符串)
+ */
+ public static void d(String szTAG, String szMessage, Object obj) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ String objStr = obj == null ? "null" : obj.toString();
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【数据对象】: ").append(objStr);
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
+ }
+ }
+
+ /**
+ * Debug 级别日志(仅数据对象)
+ * @param szTAG 标签
+ * @param obj 数据对象(自动转为字符串)
+ */
+ public static void d(String szTAG, Object obj) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ String objStr = obj == null ? "null" : obj.toString();
+ String message = "【数据对象】: " + objStr;
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, message);
+ }
+ }
+
+ // ================================= 补全所有日志重载方法(Verbose 级别) =================================
+ /**
+ * Verbose 级别日志(仅消息)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ */
+ public static void v(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, szMessage);
+ }
+ }
+
+ /**
+ * Verbose 级别日志(消息 + 数据对象)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param obj 数据对象(自动转为字符串)
+ */
+ public static void v(String szTAG, String szMessage, Object obj) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
+ String objStr = obj == null ? "null" : obj.toString();
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【数据对象】: ").append(objStr);
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, sb.toString());
+ }
+ }
+
+ /**
+ * Verbose 级别日志(仅数据对象)
+ * @param szTAG 标签
+ * @param obj 数据对象(自动转为字符串)
+ */
+ public static void v(String szTAG, Object obj) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
+ String objStr = obj == null ? "null" : obj.toString();
+ String message = "【数据对象】: " + objStr;
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, message);
+ }
+ }
+
+ // ================================= 新增:通用日志工具方法(补充调试能力) =================================
+ /**
+ * 打印当前线程信息(Debug 级别)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ */
+ public static void printThreadInfo(String szTAG, String szMessage) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
+ Thread currentThread = Thread.currentThread();
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【线程信息】: ")
+ .append("线程名=").append(currentThread.getName())
+ .append(", 线程ID=").append(currentThread.getId())
+ .append(", 线程状态=").append(currentThread.getState().name());
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
+ }
+ }
+
+ /**
+ * 打印 Map 数据(Debug 级别,格式化输出,便于查看)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param map 要打印的 Map 数据
+ */
+ public static void printMap(String szTAG, String szMessage, Map map) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && map != null) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【Map 数据】(size=").append(map.size()).append("):");
+ for (Map.Entry entry : map.entrySet()) {
+ String keyStr = entry.getKey() == null ? "null" : entry.getKey().toString();
+ String valueStr = entry.getValue() == null ? "null" : entry.getValue().toString();
+ sb.append("\n ").append(keyStr).append(" = ").append(valueStr);
+ }
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
+ }
+ }
+
+ /**
+ * 打印 List 数据(Debug 级别,格式化输出)
+ * @param szTAG 标签
+ * @param szMessage 日志消息
+ * @param list 要打印的 List 数据
+ */
+ public static void printList(String szTAG, String szMessage, List list) {
+ if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && list != null) {
+ StringBuilder sb = new StringBuilder(szMessage);
+ sb.append("\n【List 数据】(size=").append(list.size()).append("):");
+ for (int i = 0; i < list.size(); i++) {
+ T item = list.get(i);
+ String itemStr = item == null ? "null" : item.toString();
+ sb.append("\n 索引").append(i).append(" = ").append(itemStr);
+ }
+ saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString());
+ }
+ }
+
+ // ================================= 私有工具方法(异常/堆栈信息格式化) =================================
+ /**
+ * 格式化异常信息(提取异常类型、消息、简化堆栈)
+ * @param e 异常对象
+ * @return 格式化后的异常字符串
+ */
+ private static String getExceptionInfo(Exception e) {
+ if (e == null) {
+ return "异常对象为null";
+ }
+ StringBuilder sb = new StringBuilder();
+ // 异常类型 + 异常消息
+ sb.append(e.getClass().getSimpleName()).append(" : ").append(e.getMessage() == null ? "无异常消息" : e.getMessage());
+ // 简化堆栈(取前5行,避免日志过长)
+ StackTraceElement[] stackTrace = e.getStackTrace();
+ if (stackTrace != null && stackTrace.length > 0) {
+ sb.append("\n 简化堆栈(前5行):");
+ int limit = Math.min(stackTrace.length, 5);
+ for (int i = 0; i < limit; i++) {
+ sb.append("\n ").append(stackTrace[i].toString());
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 格式化堆栈信息(提取关键调用链路)
+ * @param stackTrace 堆栈数组
+ * @return 格式化后的堆栈字符串
+ */
+ private static String getStackTraceInfo(StackTraceElement[] stackTrace) {
+ if (stackTrace == null || stackTrace.length == 0) {
+ return "堆栈信息为空";
+ }
+ StringBuilder sb = new StringBuilder();
+ // 过滤 LogUtils 内部调用,取真实业务调用链路(前8行)
+ int count = 0;
+ for (StackTraceElement element : stackTrace) {
+ // 跳过 LogUtils 自身的堆栈(避免冗余)
+ if (element.getClassName().contains("cc.winboll.studio.libappbase.LogUtils")) {
+ continue;
+ }
+ sb.append("\n ").append(element.getClassName()).append(".")
+ .append(element.getMethodName()).append("(")
+ .append(element.getFileName()).append(":").append(element.getLineNumber()).append(")");
+ count++;
+ if (count >= 8) { // 限制堆栈长度,避免日志过大
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ // ================================= 原有核心方法(保留并优化) =================================
+ /**
+ * 日志文件保存函数(优化:增加异常捕获完整性,避免流泄漏)
+ * @param szTAG 标签
* @param logLevel 日志级别
- * @param message 日志内容
+ * @param szMessage 日志消息
*/
- private static void saveLog(String tag, LOG_LEVEL logLevel, String message) {
- BufferedWriter writer = null;
+ static void saveLog(String szTAG, LogUtils.LOG_LEVEL logLevel, String szMessage) {
+ BufferedWriter out = null;
try {
- // 以追加模式打开日志文件,UTF-8 编码
- writer = new BufferedWriter(
- new OutputStreamWriter(
- new FileOutputStream(sLogFile, true),
- "UTF-8"
- )
- );
- // 拼接日志内容(级别 + 时间 + TAG + 消息)
- String logContent = String.format(
- "[%s] %s [%s]\n%s\n",
- logLevel.name(), // 日志级别(如 Debug)
- sSimpleDateFormat.format(System.currentTimeMillis()), // 时间戳
- tag, // TAG 名称
- message // 日志内容
- );
- // 写入文件
- writer.write(logContent);
+ // 确保日志文件存在(创建父目录 + 文件)
+ if (!_mfLogCatchFile.exists()) {
+ File parentDir = _mfLogCatchFile.getParentFile();
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ _mfLogCatchFile.createNewFile();
+ }
+ // 追加写入日志(UTF-8编码,避免中文乱码)
+ out = new BufferedWriter(new OutputStreamWriter(
+ new FileOutputStream(_mfLogCatchFile, true), "UTF-8"));
+ String logLine = "[" + logLevel + "] "
+ + mSimpleDateFormat.format(System.currentTimeMillis())
+ + " [" + szTAG + "]\n"
+ + szMessage + "\n\n"; // 增加空行,区分不同日志
+ out.write(logLine);
+ out.flush(); // 强制刷新,确保日志及时写入
} catch (IOException e) {
- // 日志写入失败时,输出内部调试日志
- d(TAG, "日志写入失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+ // 日志写入失败时,打印系统日志(避免递归调用)
+ android.util.Log.e(TAG, "日志写入失败: " + e.getMessage());
} finally {
- // 关闭流,避免资源泄漏(Java 7 手动关闭,不使用 try-with-resources)
- if (writer != null) {
+ // 关闭流,避免资源泄漏(Java 7 手动关闭,无 try-with-resources)
+ if (out != null) {
try {
- writer.close();
+ out.close();
} catch (IOException e) {
- e.printStackTrace();
+ android.util.Log.e(TAG, "流关闭失败: " + e.getMessage());
}
}
}
}
/**
- * 加载历史日志(读取日志文件所有内容)
- * @return 历史日志字符串(空字符串表示文件不存在或读取失败)
+ * 历史日志加载函数(优化:增加流关闭,避免内存泄漏)
+ * @return 日志内容字符串(空串表示无日志)
*/
public static String loadLog() {
- // 日志文件不存在,返回空
- if (sLogFile == null || !sLogFile.exists()) {
- return "";
+ if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) {
+ return "日志文件不存在";
}
-
- StringBuilder logContent = new StringBuilder();
- BufferedReader reader = null;
+ StringBuffer sb = new StringBuffer();
+ BufferedReader in = null;
try {
- // 以 UTF-8 编码读取日志文件
- reader = new BufferedReader(
- new InputStreamReader(
- new FileInputStream(sLogFile),
- "UTF-8"
- )
- );
- String line;
- // 逐行读取并拼接(Java 7 普通 while 循环)
- while ((line = reader.readLine()) != null) {
- logContent.append(line).append("\n");
+ in = new BufferedReader(new InputStreamReader(
+ new FileInputStream(_mfLogCatchFile), "UTF-8"));
+ String line = "";
+ while ((line = in.readLine()) != null) {
+ sb.append(line);
+ sb.append("\n");
}
} catch (IOException e) {
- // 读取失败时,输出内部调试日志
- d(TAG, "日志读取失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+ sb.append("日志加载失败: ").append(e.getMessage());
+ LogUtils.e(TAG, "日志加载异常", e);
} finally {
// 关闭流,避免资源泄漏
- if (reader != null) {
+ if (in != null) {
try {
- reader.close();
+ in.close();
} catch (IOException e) {
- e.printStackTrace();
+ LogUtils.e(TAG, "流关闭异常", e);
}
}
}
- return logContent.toString();
+ return sb.toString();
}
/**
- * 清理历史日志(清空日志文件内容)
+ * 清理日志函数(优化:支持清空日志,避免文件过大)
*/
public static void cleanLog() {
- if (sLogFile == null || !sLogFile.exists()) {
+ if (_mfLogCatchFile == null) {
+ LogUtils.d(TAG, "日志文件未初始化,无需清理");
return;
}
-
try {
- // 写入空字符串到文件,实现清空(Java 7 手动处理流)
- BufferedWriter writer = new BufferedWriter(
- new OutputStreamWriter(
- new FileOutputStream(sLogFile),
- "UTF-8"
- )
- );
- writer.write("");
- writer.close();
+ // 清空文件内容(覆盖写入空字符串)
+ BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
+ new FileOutputStream(_mfLogCatchFile, false), "UTF-8"));
+ out.write("");
+ out.flush();
+ out.close();
+ LogUtils.d(TAG, "日志已清空,文件路径: " + _mfLogCatchFile.getPath());
} catch (IOException e) {
- // 清空失败时,输出内部调试日志(带调用栈)
- d(TAG, e, Thread.currentThread().getStackTrace());
+ LogUtils.e(TAG, "日志清空失败", e);
}
}
/**
- * 辅助方法:创建文件夹(不存在则创建)
- * @param dir 目标文件夹
+ * 检查日志文件大小(新增:避免日志文件过大占用内存)
+ * @param maxSizeMB 最大允许大小(MB)
+ * @return true:文件超过限制;false:文件大小正常
*/
- private static void createDirIfNotExists(File dir) {
- if (dir != null && !dir.exists()) {
- dir.mkdirs();
+ public static boolean checkLogFileSize(int maxSizeMB) {
+ if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) {
+ return false;
}
+ // 转换为字节(1MB = 1024*1024 字节)
+ long maxSizeByte = maxSizeMB * 1024 * 1024;
+ long fileSize = _mfLogCatchFile.length();
+ LogUtils.d(TAG, String.format("日志文件大小: %.2f MB(限制: %d MB)",
+ fileSize / (1024.0 * 1024), maxSizeMB));
+ return fileSize > maxSizeByte;
+ }
+
+ /**
+ * 初始化检查(新增:快速判断 LogUtils 是否初始化完成)
+ * @return true:已初始化;false:未初始化
+ */
+ public static boolean isInited() {
+ return _IsInited;
+ }
+
+ /**
+ * 显示短提示(新增:日志+Toast联动,调试时快速提示)
+ * @param context 上下文
+ * @param message 提示内容
+ */
+ public static void showShortToast(final Context context, final String message) {
+ if (context == null || message == null) {
+ return;
+ }
+ // 主线程显示Toast,避免子线程崩溃
+ if (Thread.currentThread().getId() == android.os.Process.myTid()) {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ } else {
+ ((android.app.Activity) context).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ // 同时写入日志
+ LogUtils.d(TAG, "Toast提示: " + message);
}
}
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java
index af50d0f..345489f 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java
@@ -1,5 +1,10 @@
package cc.winboll.studio.libappbase;
+/**
+ * @Author ZhanGSKen@QQ.COM
+ * @Date 2024/08/12 14:36:18
+ * @Describe 日志视图类,继承 RelativeLayout 类。
+ */
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -21,6 +26,9 @@ import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.R;
+import cc.winboll.studio.libappbase.views.HorizontalListView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -28,49 +36,27 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
-/**
- * @Author ZhanGSKen
- * @Date 2024/08/12 14:36:18
- * @Describe 日志可视化自定义 View(继承 RelativeLayout)
- * 核心功能:日志展示、日志级别筛选、TAG 过滤(启用/禁用)、TAG 搜索定位、日志清理/复制、视图交互控制
- * 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新
- */
public class LogView extends RelativeLayout {
- /** 当前 View 的日志 TAG(用于调试输出) */
public static final String TAG = "LogView";
- /** 日志处理中标志(避免并发刷新,volatile 保证多线程可见性) */
- private volatile boolean mIsHandling;
- /** 新日志添加标志(标记有未处理的新日志,volatile 保证多线程可见性) */
- private volatile boolean mIsAddNewLog;
+ public volatile boolean mIsHandling;
+ public volatile boolean mIsAddNewLog;
- /** 上下文对象(用于布局加载、系统服务获取) */
- private Context mContext;
- /** 日志滚动视图(包裹日志文本,支持垂直滚动) */
- private ScrollView mLogScrollView;
- /** 日志文本展示控件(显示所有日志内容) */
- private TextView mLogTextView;
- /** TAG 搜索输入框(用于搜索并定位目标 TAG) */
- private EditText mTagSearchEt;
- /** 文本选择开关(控制是否允许选中日志文本) */
- private CheckBox mTextSelectableCb;
- /** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */
- private CheckBox mSelectAllTagCb;
- /** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */
- private TAGListAdapter mTagListAdapter;
- /** 日志监听线程(监听日志文件变化,触发视图刷新) */
- private LogViewThread mLogViewThread;
- /** 日志视图 Handler(主线程更新 UI,避免跨线程操作) */
- private LogViewHandler mLogViewHandler;
- /** 日志级别选择下拉框(用于切换全局日志输出级别) */
- private Spinner mLogLevelSpinner;
- /** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner) */
- private ArrayAdapter mLogLevelAdapter;
- /** TAG 水平列表视图(横向展示所有 TAG,支持滚动) */
- private HorizontalListView mTagHorizontalListView;
+ Context mContext;
+ ScrollView mScrollView;
+ TextView mTextView;
+ EditText metTagSearch;
+ CheckBox mSelectableCheckBox;
+ CheckBox mSelectAllTAGCheckBox;
+ TAGListAdapter mTAGListAdapter;
+ LogViewThread mLogViewThread;
+ LogViewHandler mLogViewHandler;
+ Spinner mLogLevelSpinner;
+ ArrayAdapter mLogLevelSpinnerAdapter;
+ // 标签列表
+ HorizontalListView mListViewTags;
- // ====================== 构造方法(初始化视图) ======================
public LogView(Context context) {
super(context);
initView(context);
@@ -91,307 +77,258 @@ public class LogView extends RelativeLayout {
initView(context);
}
- /**
- * 启动日志监听与展示
- * 1. 初始化并启动 LogViewThread(监听日志文件变化);
- * 2. 初始加载并展示日志内容。
- */
public void start() {
- mLogViewThread = new LogViewThread(this);
+ mLogViewThread = new LogViewThread(LogView.this);
mLogViewThread.start();
- showAndScrollLogView(); // 初始显示日志并滚动到底部
+ // 显示日志
+ showAndScrollLogView();
}
- /**
- * 滚动日志到底部(确保最新日志可见)
- * 运行在主线程,通过 post 提交 Runnable 避免 UI 线程阻塞
- */
- private void scrollLogToBottom() {
- mLogScrollView.post(new Runnable() {
- @Override
- public void run() {
- // 滚动到 ScrollView 底部(FOCUS_DOWN 表示聚焦到底部)
- mLogScrollView.fullScroll(ScrollView.FOCUS_DOWN);
- // 标记日志处理完成
- mLogViewHandler.setIsHandling(false);
- // 检查是否有未处理的新日志,有则再次触发刷新
- if (mLogViewHandler.isAddNewLog()) {
- mLogViewHandler.setIsAddNewLog(false);
- Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
- mLogViewHandler.sendMessage(refreshMsg);
- }
- }
- });
+ public void scrollLogUp() {
+ mScrollView.post(new Runnable() {
+ @Override
+ public void run() {
+ mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
+ // 日志显示结束
+ mLogViewHandler.setIsHandling(false);
+ // 检查是否添加了新日志
+ if (mLogViewHandler.isAddNewLog()) {
+ // 有新日志添加,先更改新日志标志
+ mLogViewHandler.setIsAddNewLog(false);
+ // 再次发送显示日志的显示
+ Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
+ mLogViewHandler.sendMessage(message);
+ }
+ }
+ });
}
- /**
- * 初始化视图组件(加载布局、绑定控件、设置监听)
- * @param context 上下文对象
- */
- private void initView(Context context) {
+ void initView(Context context) {
mContext = context;
- mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler
+ mLogViewHandler = new LogViewHandler();
+ // 加载视图布局
+ addView(inflate(mContext, cc.winboll.studio.libappbase.R.layout.view_log, null));
+ // 初始化日志子控件视图
+ //
+ mScrollView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogScrollViewLog);
+ mTextView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogTextViewLog);
+ metTagSearch = findViewById(cc.winboll.studio.libappbase.R.id.tagsearch_et);
+ // 获取Log Level spinner实例
+ mLogLevelSpinner = findViewById(cc.winboll.studio.libappbase.R.id.viewlogSpinner1);
- // 加载日志视图布局(R.layout.view_log 为自定义布局文件)
- View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true);
- // 绑定布局控件(通过 ID 找到对应组件)
- bindViews(rootView);
+ metTagSearch.addTextChangedListener(new TextWatcher() {
- // 设置 TAG 搜索输入框监听(实时搜索并定位 TAG)
- setupTagSearchListener();
- // 设置功能按钮监听(清理日志、复制日志)
- setupFunctionButtonListeners(rootView);
- // 设置文本选择开关监听(控制日志文本是否可选中)
- setupTextSelectableListener();
- // 初始化日志级别下拉框(绑定级别数据,设置默认值)
- initLogLevelSpinner();
- // 初始化 TAG 列表(加载所有 TAG,设置全选状态)
- initTagListView();
- // 设置默认交互模式(默认禁止子视图获取焦点,避免误触)
+ @Override
+ public void afterTextChanged(Editable editable) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int p, int p1, int p2) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ LogUtils.d(TAG, s.toString());
+ if (s.length() > 0) {
+ scrollToTag(s.toString());
+ } else {
+ HorizontalScrollView hsRoot = findViewById(R.id.viewlogHorizontalScrollView1);
+ hsRoot.smoothScrollTo(0, 0);
+ mListViewTags.resetScrollToStart();
+ }
+// mListViewTags.postDelayed(new Runnable() {
+// @Override
+// public void run() {
+// mListViewTags.scrollToItem(5);
+// }
+// }, 100);
+ }
+ // 其他方法留空或按需实现
+ });
+
+
+ (findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonClean)).setOnClickListener(new View.OnClickListener(){
+
+ @Override
+ public void onClick(View v) {
+ LogUtils.cleanLog();
+ LogUtils.d(TAG, "Log is cleaned.");
+ }
+ });
+ (findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonCopy)).setOnClickListener(new View.OnClickListener(){
+
+ @Override
+ public void onClick(View v) {
+
+ ClipboardManager cm = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
+ LogUtils.d(TAG, "Log is copied.");
+ }
+ });
+ mSelectableCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBoxSelectable);
+ mSelectableCheckBox.setOnClickListener(new View.OnClickListener(){
+ @Override
+ public void onClick(View v) {
+ if (mSelectableCheckBox.isChecked()) {
+ setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ } else {
+ setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+ }
+ });
+
+ // 设置日志级别列表
+ ArrayList adapterItems = new ArrayList<>();
+ for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) {
+ adapterItems.add(e.name());
+ }
+ // 假设你有一个字符串数组作为选项列表
+ //String[] options = {"Option 1", "Option 2", "Option 3"};
+ // 创建一个ArrayAdapter来绑定数据到spinner
+ mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource(
+ context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
+ mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ // 设置适配器并将它应用到spinner上
+ mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式
+ mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter);
+ // 为Spinner添加监听器
+ mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ //String selectedOption = mLogLevelSpinnerAdapter.getItem(position);
+ // 处理选中的选项...
+ LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
+ }
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ // 如果没有选择,则执行此操作...
+ }
+ });
+ // 获取默认值的索引
+ int defaultValueIndex = LogUtils.getLogLevel().ordinal();
+
+ if (defaultValueIndex != -1) {
+ // 如果找到了默认值,设置默认选项
+ mLogLevelSpinner.setSelection(defaultValueIndex);
+ }
+
+ // 加载标签列表
+ Map mapTAGList = LogUtils.getMapTAGList();
+ boolean isAllSelect = true;
+ for (Map.Entry entry : mapTAGList.entrySet()) {
+ if (entry.getValue() == false) {
+ isAllSelect = false;
+ break;
+ }
+ }
+ CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
+ cbALLTAG.setChecked(isAllSelect);
+
+ // 加载标签表
+ mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview);
+ mListViewTags.setVerticalOffset(10);
+ mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList);
+ mListViewTags.setAdapter(mTAGListAdapter);
+
+ // 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等
+ mTAGListAdapter.notifyDataSetChanged();
+
+ mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
+ mSelectAllTAGCheckBox.setOnClickListener(new View.OnClickListener(){
+ @Override
+ public void onClick(View v) {
+ LogUtils.setALlTAGListEnable(mSelectAllTAGCheckBox.isChecked());
+ //LogUtils.setALlTAGListEnable(false);
+ //mTAGListAdapter.notifyDataSetChanged();
+ mTAGListAdapter.reload();
+ //ToastUtils.show(String.format("onClick\nmSelectAllTAGCheckBox.isChecked() : %s", mSelectAllTAGCheckBox.isChecked()));
+ }
+ });
+
+
+ // 设置滚动时不聚焦日志
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
- /**
- * 绑定布局控件(通过 ID 查找并初始化所有子组件)
- * @param rootView 根布局视图
- */
- private void bindViews(View rootView) {
- mLogScrollView = rootView.findViewById(R.id.viewlogScrollViewLog);
- mLogTextView = rootView.findViewById(R.id.viewlogTextViewLog);
- mTagSearchEt = rootView.findViewById(R.id.tagsearch_et);
- mLogLevelSpinner = rootView.findViewById(R.id.viewlogSpinner1);
- mTextSelectableCb = rootView.findViewById(R.id.viewlogCheckBoxSelectable);
- mSelectAllTagCb = rootView.findViewById(R.id.viewlogCheckBox1);
- mTagHorizontalListView = rootView.findViewById(R.id.tags_listview);
- }
-
- /**
- * 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位)
- */
- private void setupTagSearchListener() {
- mTagSearchEt.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- String searchText = s.toString().trim();
- LogUtils.d(TAG, "TAG 搜索内容:" + searchText);
- if (!searchText.isEmpty()) {
- // 搜索文本非空,定位匹配的 TAG
- scrollToTargetTag(searchText);
- } else {
- // 搜索文本为空,重置滚动位置
- HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1);
- parentHs.smoothScrollTo(0, 0);
- mTagHorizontalListView.resetScrollToStart();
- }
- }
-
- @Override
- public void afterTextChanged(Editable s) {}
- });
- }
-
- /**
- * 设置功能按钮监听(清理日志、复制日志)
- */
- private void setupFunctionButtonListeners(View rootView) {
- // 清理日志按钮(点击清空所有历史日志)
- rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- LogUtils.cleanLog();
- LogUtils.d(TAG, "日志已清理");
- }
- });
-
- // 复制日志按钮(点击复制所有日志到剪贴板)
- rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- // 将日志内容复制到剪贴板(标签为应用包名)
- clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
- LogUtils.d(TAG, "日志已复制到剪贴板");
- }
- });
- }
-
- /**
- * 设置文本选择开关监听(控制日志文本是否可选中复制)
- */
- private void setupTextSelectableListener() {
- mTextSelectableCb.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mTextSelectableCb.isChecked()) {
- // 允许文本选择:子视图优先获取焦点
- setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
- } else {
- // 禁止文本选择:阻止子视图获取焦点
- setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- }
- }
- });
- }
-
- /**
- * 初始化日志级别下拉框(Spinner)
- * 1. 绑定 LogUtils.LOG_LEVEL 枚举数据;
- * 2. 设置默认选中当前全局日志级别;
- * 3. 监听级别变化,更新 LogUtils 全局配置。
- */
- private void initLogLevelSpinner() {
- // 从资源文件加载日志级别数组(R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应)
- mLogLevelAdapter = ArrayAdapter.createFromResource(
- mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
- // 设置下拉列表样式
- mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- mLogLevelSpinner.setAdapter(mLogLevelAdapter);
-
- // 监听下拉框选择变化
- mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> parent, View view, int position, long id) {
- // 根据选择的位置设置全局日志级别(position 与 LOG_LEVEL 枚举索引对应)
- LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
- }
-
- @Override
- public void onNothingSelected(AdapterView> parent) {}
- });
-
- // 设置默认选中当前日志级别
- int defaultLevelIndex = LogUtils.getLogLevel().ordinal();
- if (defaultLevelIndex >= 0) {
- mLogLevelSpinner.setSelection(defaultLevelIndex);
- }
- }
-
- /**
- * 初始化 TAG 水平列表
- * 1. 加载 LogUtils 中的所有 TAG 及其启用状态;
- * 2. 初始化 TAG 列表适配器;
- * 3. 设置全选 TAG 开关监听。
- */
- private void initTagListView() {
- // 获取 LogUtils 中的 TAG 启用状态映射表
- Map tagEnableMap = LogUtils.getTagEnableMap();
- // 判断是否所有 TAG 都已启用(初始化全选开关状态)
- boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap);
- mSelectAllTagCb.setChecked(isAllTagEnabled);
-
- // 初始化 TAG 水平列表(设置垂直偏移,绑定适配器)
- mTagHorizontalListView.setVerticalOffset(10);
- mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap);
- mTagHorizontalListView.setAdapter(mTagListAdapter);
- mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据
-
- // 全选 TAG 开关监听(点击时启用/禁用所有 TAG)
- mSelectAllTagCb.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- boolean isSelectAll = mSelectAllTagCb.isChecked();
- LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态
- mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图
- }
- });
- }
-
- /**
- * 判断是否所有 TAG 都已启用
- * @param tagEnableMap TAG 启用状态映射表
- * @return true:所有 TAG 均启用;false:存在未启用的 TAG
- */
- private boolean isAllTagsEnabled(Map tagEnableMap) {
- for (Map.Entry entry : tagEnableMap.entrySet()) {
- if (!entry.getValue()) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * 更新日志视图(由 LogViewThread 触发,通知有新日志)
- * 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新
- */
public void updateLogView() {
- if (mLogViewHandler.isHandling()) {
- // 正在处理日志刷新,标记有新日志待处理
+ if (mLogViewHandler.isHandling() == true) {
+ // 正在处理日志显示,
+ // 就先设置一个新日志标志位
+ // 以便日志显示完后,再次显示新日志内容
mLogViewHandler.setIsAddNewLog(true);
} else {
- // 发送刷新消息到主线程
- Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
- mLogViewHandler.sendMessage(refreshMsg);
+ //LogUtils.d(TAG, "LogListener showLog(String path)");
+ Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
+ mLogViewHandler.sendMessage(message);
mLogViewHandler.setIsAddNewLog(false);
}
}
- /**
- * 显示日志并滚动到底部
- * 1. 从 LogUtils 加载所有历史日志;
- * 2. 设置到文本控件并滚动到底部。
- */
- private void showAndScrollLogView() {
- mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志
- scrollLogToBottom(); // 滚动到底部,显示最新日志
+ void showAndScrollLogView() {
+ mTextView.setText(LogUtils.loadLog());
+ scrollLogUp();
}
- /**
- * 滚动到目标 TAG(根据搜索文本定位匹配的 TAG 并滚动显示)
- * @param prefix 搜索文本(TAG 前缀)
- */
- private void scrollToTargetTag(final String prefix) {
- if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) {
- LogUtils.d(TAG, "TAG 搜索参数为空,无法定位");
+ public void scrollToTag(final String prefix) {
+ if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) {
+ LogUtils.d(TAG, "参数为空,无法滚动");
return;
}
- final List tagItemList = mTagListAdapter.getItemList();
- mTagHorizontalListView.post(new Runnable() {
- @Override
- public void run() {
- int targetPosition = -1;
- // 遍历 TAG 列表,查找前缀匹配的 TAG(忽略大小写)
- for (int i = 0; i < tagItemList.size(); i++) {
- String tag = tagItemList.get(i).getTag();
- if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
- targetPosition = i;
- break;
- }
- }
+ final List itemList = mTAGListAdapter.getItemList();
- if (targetPosition != -1) {
- final int targetPositionFinal = targetPosition;
- // 延迟滚动(确保布局完成,避免滚动失效)
- mTagHorizontalListView.postDelayed(new Runnable() {
- @Override
- public void run() {
- LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal);
- mTagHorizontalListView.scrollToItem(targetPositionFinal);
- }
- }, 100);
- } else {
- LogUtils.d(TAG, "未找到匹配前缀的 TAG:" + prefix);
- }
- }
- });
+ mListViewTags.post(new Runnable() {
+ @Override
+ public void run() {
+ // 查找匹配的标签位置
+ int targetPosition = -1;
+
+ for (int i = 0; i < itemList.size(); i++) {
+ String tag = itemList.get(i).getTag();
+ if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
+ targetPosition = i;
+
+ break;
+ }
+ }
+
+ if (targetPosition != -1) {
+ // 优化滚动逻辑
+ //mListViewTags.setSelection(targetPosition);
+ //mListViewTags.invalidateViews(); // 强制刷新所有可见项
+
+ // 单独刷新目标视图
+// View targetView = mListViewTags.getChildAt(targetPosition);
+// if (targetView != null) {
+// targetView.requestLayout();
+// targetView.requestFocus();
+// }
+
+ final int scrollPosition = targetPosition;
+
+ // 延迟滚动确保布局完成
+ mListViewTags.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ LogUtils.d(TAG, String.format("scrollPosition %d", scrollPosition));
+ mListViewTags.scrollToItem(scrollPosition);
+ }
+ }, 100);
+ } else {
+ LogUtils.d(TAG, "未找到匹配的标签前缀:" + prefix);
+ }
+ }
+ });
}
- // ====================== 内部类:日志视图 Handler(主线程更新 UI) ======================
- /**
- * 日志视图 Handler(运行在主线程,处理日志刷新消息)
- * 避免跨线程操作 UI,通过标志位控制并发刷新
- */
- private class LogViewHandler extends Handler {
- /** 日志刷新消息标识 */
- private static final int MSG_LOG_REFRESH = 0;
- /** 日志处理中标志(与外部 mIsHandling 同步) */
- private volatile boolean isHandling;
- /** 新日志添加标志(与外部 mIsAddNewLog 同步) */
- private volatile boolean isAddNewLog;
+
+
+ class LogViewHandler extends Handler {
+
+ final static int MSG_LOGVIEW_UPDATE = 0;
+ volatile boolean isHandling;
+ volatile boolean isAddNewLog;
public LogViewHandler() {
setIsHandling(false);
@@ -414,32 +351,24 @@ public class LogView extends RelativeLayout {
return isAddNewLog;
}
- @Override
public void handleMessage(Message msg) {
- super.handleMessage(msg);
switch (msg.what) {
- case MSG_LOG_REFRESH:
- // 未处理日志刷新时,标记为处理中并触发显示
- if (!isHandling()) {
- setIsHandling(true);
- showAndScrollLogView();
+ case MSG_LOGVIEW_UPDATE:{
+ if (isHandling() == false) {
+ setIsHandling(true);
+ showAndScrollLogView();
+ }
+ break;
}
- break;
default:
break;
}
+ super.handleMessage(msg);
}
}
- // ====================== 内部类:TAG 数据模型(封装 TAG 名称与状态) ======================
- /**
- * TAG 列表项数据模型
- * 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定)
- */
- private class TAGItemModel {
- /** TAG 名称(如 "LogViewThread"、"LogUtils") */
+ public class TAGItemModel {
private String tag;
- /** TAG 启用状态(true:启用;false:禁用) */
private boolean isChecked;
public TAGItemModel(String tag, boolean isChecked) {
@@ -463,17 +392,18 @@ public class LogView extends RelativeLayout {
isChecked = checked;
}
- /**
- * 重写 equals 方法(按 TAG 名称判断相等)
- * @param o 比较对象
- * @return true:TAG 名称相同;false:不同
- */
+ // getter/setter...
+
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
TAGItemModel that = (TAGItemModel) o;
- // 手动处理空值比较(兼容 Java 7,不依赖 Objects.equals)
+ // 手动处理空值比较(Java 6 不支持 Objects.equals)
if (tag == null) {
return that.tag == null;
} else {
@@ -481,174 +411,106 @@ public class LogView extends RelativeLayout {
}
}
- /**
- * 重写 hashCode 方法(基于 TAG 名称生成哈希值)
- * @return 哈希值(空 TAG 返回 0)
- */
@Override
public int hashCode() {
- return tag == null ? 0 : tag.hashCode();
+ return tag == null ? 0 : tag.hashCode(); // 手动处理空值
}
}
- // ====================== 内部类:TAG 列表适配器(绑定数据与视图) ======================
- /**
- * TAG 水平列表适配器(继承 BaseAdapter)
- * 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
- */
- private class TAGListAdapter extends BaseAdapter {
- /** 上下文对象(用于加载列表项布局) */
+
+ public class TAGListAdapter extends BaseAdapter {
+
private Context context;
- /** 原始 TAG 启用状态映射表(来自 LogUtils) */
- private Map originTagMap;
- /** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
- private List tagItemList;
+ private Map mapOrigin;
+ private List itemList;
- /**
- * 构造方法(初始化数据并加载到列表)
- * @param context 上下文
- * @param tagMap TAG 启用状态映射表
- */
- public TAGListAdapter(Context context, Map tagMap) {
+ public TAGListAdapter(Context context, Map map) {
this.context = context;
- this.originTagMap = tagMap;
- loadTagData(originTagMap); // 加载并转换数据
+ mapOrigin = map;
+ loadMap(mapOrigin);
}
- /**
- * 获取 TAG 列表项数据(供外部定位 TAG 使用)
- * @return TAGItemModel 列表
- */
public List getItemList() {
- return tagItemList;
+ return itemList;
}
- // ====================== BaseAdapter 抽象方法实现 ======================
@Override
public int getCount() {
- return tagItemList == null ? 0 : tagItemList.size();
+ return itemList.size();
}
@Override
- public Object getItem(int position) {
- return tagItemList.get(position);
+ public Object getItem(int p) {
+ return itemList.get(p);
}
@Override
- public long getItemId(int position) {
- return position;
+ public long getItemId(int p) {
+ return p;
}
- /**
- * 加载 TAG 数据(将 Map 转换为 List 并排序)
- * @param tagMap TAG 启用状态映射表
- */
- private void loadTagData(Map tagMap) {
- tagItemList = new ArrayList<>();
- // 遍历 Map,转换为 TAGItemModel 并添加到列表
- for (Map.Entry entry : tagMap.entrySet()) {
- tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
+ void loadMap(Map map) {
+ itemList = new ArrayList();
+ for (Map.Entry entry : map.entrySet()) {
+ itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
}
- // 按 TAG 名称升序排序(中文排序兼容)
- Collections.sort(tagItemList, new TagAscComparator(true));
+ // 添加排序功能,按照tag进行升序排序
+ Collections.sort(itemList, new SortMapEntryByKeyString(true));
+ //Collections.sort(itemList, new SortMapEntryByKeyString(false));
}
- /**
- * 重新加载 TAG 数据(用于全选/反选后刷新列表)
- */
public void reload() {
- loadTagData(originTagMap); // 重新加载数据
- notifyDataSetChanged(); // 通知视图刷新
+ loadMap(mapOrigin);
+ super.notifyDataSetChanged();
}
- /**
- * 创建/复用列表项视图(优化性能,避免重复 inflate)
- * @param position 列表项位置
- * @param convertView 复用视图(可为 null)
- * @param parent 父容器
- * @return 列表项视图
- */
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
- // 复用视图(减少布局加载开销)
if (convertView == null) {
- // 加载列表项布局(R.layout.item_logtag 为 TAG 项自定义布局)
convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false);
holder = new ViewHolder();
- // 绑定列表项控件(TAG 文本和勾选框)
- holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1);
- holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1);
- convertView.setTag(holder); // 保存 ViewHolder 到视图
+ holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
+ holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
+ convertView.setTag(holder);
} else {
- holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder
+ holder = (ViewHolder) convertView.getTag();
}
- // 绑定数据到视图
- final TAGItemModel item = tagItemList.get(position);
- holder.tagTv.setText(item.getTag()); // 设置 TAG 名称
- holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态
+ final TAGItemModel item = itemList.get(position);
+ holder.tvText.setText(item.getTag());
+ holder.cbChecked.setChecked(item.isChecked());
+ holder.cbChecked.setOnClickListener(new View.OnClickListener(){
- // 勾选框点击监听(更新 TAG 启用状态)
- holder.tagCb.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- boolean isChecked = ((CheckBox) v).isChecked();
- // 调用 LogUtils 更新该 TAG 的启用状态
- LogUtils.setTagEnable(item.getTag(), isChecked);
- // 同步更新本地模型状态(避免刷新后状态不一致)
- item.setChecked(isChecked);
- }
- });
+ @Override
+ public void onClick(View v) {
+ LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
+ }
+ });
return convertView;
}
- /**
- * 列表项 ViewHolder(缓存控件,提升列表滑动性能)
- */
- private class ViewHolder {
- TextView tagTv; // TAG 名称文本控件
- CheckBox tagCb; // TAG 启用状态勾选框
+ public class ViewHolder {
+ TextView tvText;
+ CheckBox cbChecked;
}
}
- // ====================== 内部类:TAG 排序比较器(中文兼容) ======================
- /**
- * TAG 名称排序比较器(实现 Comparator)
- * 支持中文排序(基于系统默认中文 Locale),可选择升序/降序
- */
- private class TagAscComparator implements Comparator {
- /** 排序方向(true:升序;false:降序) */
- private boolean isAsc;
- /** 中文排序器(兼容中文汉字排序) */
- private Collator chineseCollator = Collator.getInstance(java.util.Locale.CHINA);
-
- public TagAscComparator(boolean isAsc) {
- this.isAsc = isAsc;
+ class SortMapEntryByKeyString implements Comparator {
+ private boolean mIsDesc = true;
+ // isDesc 是否降序排列
+ public SortMapEntryByKeyString(boolean isDesc) {
+ mIsDesc = isDesc;
}
-
- /**
- * 比较两个 TAGItemModel(按 TAG 名称排序)
- * @param o1 第一个比较对象
- * @param o2 第二个比较对象
- * @return 比较结果(正数:o1 在 o2 后;负数:o1 在 o2 前;0:相等)
- */
+ Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
@Override
public int compare(TAGItemModel o1, TAGItemModel o2) {
- String tag1 = o1.getTag();
- String tag2 = o2.getTag();
- // 处理空值(空 TAG 排在最前)
- if (tag1 == null) return -1;
- if (tag2 == null) return 1;
-
- // 根据排序方向返回比较结果
- if (isAsc) {
- return chineseCollator.compare(tag1, tag2); // 升序
+ if (mIsDesc) {
+ return o1.getTag().compareTo(o2.getTag());
} else {
- return chineseCollator.compare(tag2, tag1); // 降序
+ return o2.getTag().compareTo(o1.getTag());
}
}
}
}
-
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java
index f4586ff..a69fc37 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java
@@ -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;
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java
new file mode 100644
index 0000000..00e3842
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java
@@ -0,0 +1,264 @@
+package cc.winboll.studio.libappbase.utils;
+
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.text.TextUtils;
+
+import cc.winboll.studio.libappbase.CrashHandler;
+import cc.winboll.studio.libappbase.GlobalCrashActivity;
+import cc.winboll.studio.libappbase.LogUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/29 21:12
+ * @Describe 应用崩溃处理通知实用工具集(类库兼容版)
+ * 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
+ * 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
+ */
+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(唯一) */
+ public static final int CRASH_NOTIFY_ID = 0x001;
+ /** Android 12 对应 API 版本号(31) */
+ private static final int API_LEVEL_ANDROID_12 = 31;
+ /** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */
+ private static final int FLAG_IMMUTABLE = 0x00000040;
+
+ /** 通知内容最大行数(控制在3行,超出部分省略) */
+ private static final int NOTIFICATION_MAX_LINES = 3;
+
+
+ /**
+ * 处理未捕获异常(核心方法,类库入口)
+ * 改进点:新增宿主包名参数,移除类库对固定包名的依赖
+ * @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
+ * @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity)
+ * @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
+ */
+ public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) {
+ // 1. 校验核心参数(类库场景必须严格校验,避免空指针)
+ if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
+ LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")");
+ return;
+ }
+
+ // 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
+ String hostAppName = getHostAppName(hostApp, hostPackageName);
+
+ // 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity)
+ sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
+ }
+
+ /**
+ * 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
+ * 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
+ * @param hostApp 宿主应用的 Application 实例
+ * @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
+ */
+ public static void handleUncaughtException(Application hostApp, Intent intent) {
+ // 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
+ String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
+ if (TextUtils.isEmpty(hostPackageName)) {
+ hostPackageName = hostApp.getPackageName();
+ LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName);
+ }
+
+ // 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
+ String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
+
+ // 调用核心方法处理
+ handleUncaughtException(hostApp, hostPackageName, errorLog);
+ }
+
+ /**
+ * 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
+ * @param hostContext 宿主应用的上下文(Application 实例)
+ * @param hostPackageName 宿主应用的包名
+ * @return 宿主应用名称(读取失败返回 "未知应用")
+ */
+ private static String getHostAppName(Context hostContext, String hostPackageName) {
+ try {
+ // 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
+ return hostContext.getPackageManager().getApplicationLabel(
+ hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
+ ).toString();
+ } catch (Exception e) {
+ LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e);
+ return "未知应用";
+ }
+ }
+
+ /**
+ * 发送崩溃通知到宿主系统通知栏(类库兼容版)
+ * 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
+ * @param hostContext 宿主应用的上下文(Application 实例)
+ * @param hostPackageName 宿主应用的包名
+ * @param hostAppName 宿主应用的名称(用于通知标题)
+ * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
+ */
+ private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
+ // 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
+ NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")");
+ return;
+ }
+
+ // 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createCrashNotifyChannel(hostContext, notificationManager);
+ }
+
+ // 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
+ PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
+ if (jumpIntent == null) {
+ LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")");
+ return;
+ }
+
+ // 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
+ Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
+
+ // 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
+ notificationManager.notify(CRASH_NOTIFY_ID, notification);
+ LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
+ }
+
+ /**
+ * 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突)
+ * @param hostContext 宿主应用的上下文
+ * @param notificationManager 宿主的通知管理器
+ */
+ private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
+ // 仅 Android 8.0+ 执行(避免低版本报错)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // 构建通知渠道(归属宿主应用,描述明确类库用途)
+ android.app.NotificationChannel channel = new android.app.NotificationChannel(
+ CRASH_NOTIFY_CHANNEL_ID,
+ CRASH_NOTIFY_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
+ // 注册渠道到宿主的通知管理器,确保渠道归属宿主
+ notificationManager.createNotificationChannel(channel);
+ LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")");
+ }
+ }
+
+ /**
+ * 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
+ * 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity;
+ * 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
+ * 3. 使用宿主上下文,避免类库上下文导致的适配问题。
+ * @param hostContext 宿主应用的上下文
+ * @param hostPackageName 宿主应用的包名
+ * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
+ * @return 跳转崩溃详情页的 PendingIntent
+ */
+ private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
+ try {
+ // 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
+ Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
+ // 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰)
+ crashIntent.setPackage(hostPackageName);
+ // 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配)
+ crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
+ // 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
+ crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ // 2. 构建 PendingIntent(使用宿主上下文,适配高版本)
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
+ flags |= FLAG_IMMUTABLE;
+ }
+
+ return PendingIntent.getActivity(
+ hostContext,
+ CRASH_NOTIFY_ID, // 用通知ID作为请求码,确保唯一(避免意图复用)
+ crashIntent,
+ flags
+ );
+ } catch (Exception e) {
+ LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e);
+ return null;
+ }
+ }
+
+ /**
+ * 构建通知实例(类库兼容版)
+ * 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
+ * @param hostContext 宿主应用的上下文
+ * @param hostAppName 宿主应用的名称(通知标题)
+ * @param errorLog 崩溃日志(通知内容)
+ * @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity)
+ * @return 构建完成的 Notification 对象
+ */
+ private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
+ // 兼容 Android 8.0+:指定宿主的通知渠道ID
+ Notification.Builder builder = new Notification.Builder(hostContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
+ }
+
+ // 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建)
+ Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
+ bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
+ bigTextStyle.bigText(errorLog);
+ bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
+ builder.setStyle(bigTextStyle);
+
+ // 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
+ builder
+ // 关键:使用宿主应用的小图标(避免类库图标显示异常)
+ .setSmallIcon(hostContext.getApplicationInfo().icon)
+ .setContentTitle(hostAppName + " 崩溃")
+ .setContentText(getShortContent(errorLog)) // 3行内缩略文本
+ .setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
+ .setAutoCancel(true) // 点击后自动关闭
+ .setWhen(System.currentTimeMillis())
+ .setPriority(Notification.PRIORITY_DEFAULT);
+
+ // 适配 Android 4.1+:确保在宿主应用中正常显示
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return builder.build();
+ } else {
+ return builder.getNotification();
+ }
+ }
+
+ /**
+ * 辅助方法:截取日志文本,确保显示在3行内(通用逻辑,无包名依赖)
+ * @param content 完整崩溃日志
+ * @return 3行内的缩略文本
+ */
+ private static String getShortContent(String content) {
+ if (content == null || content.isEmpty()) {
+ return "无崩溃日志";
+ }
+ int maxLength = 80; // 估算3行字符数(可根据需求调整)
+ return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
+ }
+
+ /**
+ * 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展)
+ * @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖)
+ */
+ public static void release(Context hostContext) {
+ LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")");
+ }
+}
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java
new file mode 100644
index 0000000..4e41357
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java
@@ -0,0 +1,129 @@
+package cc.winboll.studio.libappbase.views;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/03/12 12:29:01
+ * @Describe 水平布局的 ListView
+ */
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Scroller;
+import cc.winboll.studio.libappbase.LogUtils;
+
+public class HorizontalListView extends ListView {
+ public static final String TAG = "HorizontalListView";
+ private int verticalOffset = 0;
+ private Scroller scroller;
+ private int totalWidth;
+
+ public HorizontalListView(Context context) {
+ super(context);
+ init();
+ }
+
+ public HorizontalListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ scroller = new Scroller(getContext());
+ setHorizontalScrollBarEnabled(true);
+ setVerticalScrollBarEnabled(false);
+ }
+
+ public void setVerticalOffset(int verticalOffset) {
+ this.verticalOffset = verticalOffset;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ int childCount = getChildCount();
+ int left = getPaddingLeft();
+ int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+ totalWidth = left;
+
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ int width = child.getMeasuredWidth();
+ int height = child.getMeasuredHeight();
+ child.layout(left, verticalOffset, left + width, verticalOffset + height);
+ left += width;
+ }
+ totalWidth = left + getPaddingRight();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
+ }
+
+ @Override
+ public void computeScroll() {
+ if (scroller.computeScrollOffset()) {
+ scrollTo(scroller.getCurrX(), scroller.getCurrY());
+ postInvalidate();
+ }
+ }
+
+ public void smoothScrollTo(int x, int y) {
+ int dx = x - getScrollX();
+ int dy = y - getScrollY();
+ scroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); // 300ms平滑动画
+ invalidate();
+ }
+
+ @Override
+ public int computeHorizontalScrollRange() {
+ return totalWidth;
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset() {
+ return getScrollX();
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent() {
+ return getWidth();
+ }
+
+ public void scrollToItem(int position) {
+ if (position < 0 || position >= getChildCount()) {
+ LogUtils.d(TAG, "无效的position: " + position);
+ return;
+ }
+
+ View targetView = getChildAt(position);
+ int targetLeft = targetView.getLeft();
+ int scrollX = targetLeft - getPaddingLeft();
+
+ // 修正最大滚动范围计算
+ int maxScrollX = totalWidth;
+ scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
+
+ // 强制重新布局和绘制
+ requestLayout();
+ invalidateViews();
+ smoothScrollTo(scrollX, 0);
+ LogUtils.d(TAG, String.format("滚动到position: %d, scrollX: %d computeHorizontalScrollRange() %d", position, scrollX, computeHorizontalScrollRange()));
+ }
+
+ public void resetScrollToStart() {
+ // 强制重新布局和绘制
+ requestLayout();
+ invalidateViews();
+ smoothScrollTo(0, 0);
+ }
+}
+
diff --git a/libappbase/src/main/res/drawable/ic_content_copy.xml b/libappbase/src/main/res/drawable/ic_content_copy.xml
new file mode 100644
index 0000000..0a8394f
--- /dev/null
+++ b/libappbase/src/main/res/drawable/ic_content_copy.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/libappbase/src/main/res/layout/view_log.xml b/libappbase/src/main/res/layout/view_log.xml
index 77c1b56..cb03dba 100644
--- a/libappbase/src/main/res/layout/view_log.xml
+++ b/libappbase/src/main/res/layout/view_log.xml
@@ -99,7 +99,7 @@
android:layout_weight="1.0"
android:id="@+id/viewlogHorizontalScrollView1">
-
diff --git a/libappbase/src/main/res/values/colors.xml b/libappbase/src/main/res/values/colors.xml
index 87d3836..3526edd 100644
--- a/libappbase/src/main/res/values/colors.xml
+++ b/libappbase/src/main/res/values/colors.xml
@@ -4,4 +4,5 @@
#FF005C12
#FF8DFFA2
#FFFFFB8D
+