diff --git a/appbase/build.gradle b/appbase/build.gradle
index f1adbe8b..a67edf5d 100644
--- a/appbase/build.gradle
+++ b/appbase/build.gradle
@@ -19,18 +19,21 @@ def genVersionName(def versionName){
android {
- compileSdkVersion 32
- buildToolsVersion "32.0.0"
+ // 1. compileSdkVersion:必须 ≥ targetSdkVersion,建议直接等于 targetSdkVersion(30)
+ compileSdkVersion 30
+
+ // 2. buildToolsVersion:需匹配 compileSdkVersion,建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion)
+ buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
defaultConfig {
applicationId "cc.winboll.studio.appbase"
- minSdkVersion 24
+ minSdkVersion 23
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
- versionName "15.10"
+ versionName "15.11"
if(true) {
versionName = genVersionName("${versionName}")
}
diff --git a/appbase/build.properties b/appbase/build.properties
index 8e51c0ba..5d5b92ed 100644
--- a/appbase/build.properties
+++ b/appbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Fri Sep 26 05:36:14 HKT 2025
-stageCount=9
+#Tue Nov 18 07:02:48 GMT 2025
+stageCount=1
libraryProject=libappbase
-baseVersion=15.10
-publishVersion=15.10.8
-buildCount=0
-baseBetaVersion=15.10.9
+baseVersion=15.11
+publishVersion=15.11.0
+buildCount=7
+baseBetaVersion=15.11.1
diff --git a/appbase/src/main/AndroidManifest.xml b/appbase/src/main/AndroidManifest.xml
index ccecd360..284d7afd 100644
--- a/appbase/src/main/AndroidManifest.xml
+++ b/appbase/src/main/AndroidManifest.xml
@@ -3,9 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase">
-
-
-
* @Date 2025/01/05 09:54:42
- * @Describe APPbase 应用类
+ * @Describe 应用全局入口类(继承基础库 GlobalApplication)
+ * 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
*/
-import android.content.IntentFilter;
-import cc.winboll.studio.libappbase.GlobalApplication;
-
public class App extends GlobalApplication {
+ /** 当前应用类的日志 TAG(用于调试输出,标识日志来源) */
public static final String TAG = "App";
-
+
+ /**
+ * 应用创建时回调(全局初始化入口)
+ * 在应用进程启动时执行,仅调用一次,用于初始化全局工具类、第三方库等
+ */
@Override
public void onCreate() {
- super.onCreate();
+ super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
+ // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
+ ToastUtils.init(getApplicationContext());
+ }
+
+ /**
+ * 应用终止时回调(资源释放入口)
+ * 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行
+ * 用于释放全局资源,避免内存泄漏
+ */
+ @Override
+ public void onTerminate() {
+ super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放)
+ // 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏)
+ ToastUtils.release();
}
}
+
diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
index 0a45ba63..387fab3f 100644
--- a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
+++ b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
@@ -8,67 +8,133 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.widget.Toast;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
import cc.winboll.studio.libappbase.LogActivity;
+import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
+/**
+ * @Author ZhanGSKen
+ * @Date 未标注(建议补充创建日期)
+ * @Describe 应用主界面 Activity(入口界面)
+ * 包含功能测试按钮(崩溃测试、日志查看、Toast测试)、顶部工具栏(菜单功能),是应用交互的核心入口
+ */
public class MainActivity extends Activity {
+ /** 当前 Activity 的日志 TAG(用于调试输出,标识日志来源) */
public static final String TAG = "MainActivity";
- Toolbar mToolbar;
+ /** 顶部工具栏(用于展示标题、菜单,绑定布局中的 Toolbar 控件) */
+ private Toolbar mToolbar;
+ /**
+ * Activity 创建时回调(初始化界面)
+ * 在 Activity 首次创建时执行,用于加载布局、初始化控件、设置事件监听
+ * @param savedInstanceState 保存 Activity 状态的 Bundle(如屏幕旋转时的数据恢复)
+ */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- ToastUtils.show("onCreate");
- setContentView(R.layout.activity_main);
+ ToastUtils.show("onCreate"); // 显示 Activity 创建提示(调试用)
+ setContentView(R.layout.activity_main); // 加载主界面布局
+ // 初始化 Toolbar 并设置为 ActionBar
mToolbar = findViewById(R.id.toolbar);
- setActionBar(mToolbar);
+ setActionBar(mToolbar); // 将 Toolbar 替代系统默认 ActionBar
}
+ /**
+ * 创建菜单时回调(加载工具栏菜单)
+ * 初始化 ActionBar 菜单,加载自定义菜单布局
+ * @param menu 菜单对象(用于承载菜单项)
+ * @return true:显示菜单;false:不显示菜单
+ */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
+ // 加载菜单布局(R.menu.toolbar_main 为自定义菜单文件)
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return super.onCreateOptionsMenu(menu);
}
+ /**
+ * 菜单 item 点击时回调(处理菜单事件)
+ * 响应 Toolbar 菜单项的点击事件,执行对应业务逻辑
+ * @param item 被点击的菜单项
+ * @return true:消费点击事件;false:不消费(传递给父类)
+ */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.item_home : {
- openWebsiteInBrowser(this);
- }
- }
- // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
+ switch (item.getItemId()) {
+ case R.id.item_home:
+ // 点击 "首页/官网" 菜单项,唤起浏览器打开指定网站
+ openWebsiteInBrowser(this);
+ break;
+ // 可扩展其他菜单项(如设置、关于等)的处理逻辑
+ }
return super.onOptionsItemSelected(item);
}
+ /**
+ * 崩溃测试按钮点击事件(触发应用崩溃,用于调试异常捕获)
+ * 故意执行非法操作(循环获取不存在的字符串资源),强制应用崩溃
+ * @param view 触发事件的 View(对应布局中的崩溃测试按钮)
+ */
public void onCrashTest(View view) {
- for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
- getString(i);
- }
+ // 循环从 Integer.MIN_VALUE 到 Integer.MAX_VALUE,获取不存在的字符串资源 ID,触发崩溃
+ for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
+ getString(i); // i 超出资源 ID 范围,抛出 Resources.NotFoundException 导致崩溃
+ }
}
+ /**
+ * 日志测试按钮点击事件(打开日志查看界面)
+ * 启动 LogActivity,用于查看应用运行日志
+ * @param view 触发事件的 View(对应布局中的日志测试按钮)
+ */
public void onLogTest(View view) {
+ // 启动日志查看 Activity(通过静态方法传入上下文,简化跳转逻辑)
LogActivity.startLogActivity(this);
}
- /**
- * 唤起默认浏览器打开指定网站
- * @param context 上下文(如 Activity.this)
- */
- public void openWebsiteInBrowser(Context context) {
- // 目标网站地址
- String url = "https://www.winboll.cc";
- // 构建打开浏览器的意图
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- // 设置标志:避免创建新的任务栈(可选,按需求调整)
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
-
- }
+ /**
+ * Toast 工具测试按钮点击事件(测试全局 Toast 功能)
+ * 测试主线程、子线程中 Toast 的显示效果,验证 ToastUtils 的可用性
+ * @param view 触发事件的 View(对应布局中的 Toast 测试按钮)
+ */
+ public void onToastUtilsTest(View view) {
+ LogUtils.d(TAG, "onToastUtilsTest"); // 打印调试日志,标识进入 Toast 测试
+ ToastUtils.show("Hello, WinBoLL!"); // 主线程显示 Toast
+
+ // 开启子线程,延迟 2 秒后显示 Toast(测试子线程 Toast 兼容性)
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000); // 线程休眠 2 秒
+ // 若 ToastUtils 已处理主线程切换,此处可直接调用;否则需通过 Handler 切换到主线程
+ ToastUtils.show("Thread.sleep(2000);ToastUtils.show...");
+ } catch (InterruptedException e) {
+ // 捕获线程中断异常(如线程被销毁时),不做处理(测试场景)
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * 唤起系统默认浏览器打开指定网站(跳转至应用官网)
+ * 通过 Intent.ACTION_VIEW 隐式意图,触发浏览器打开目标 URL
+ * @param context 上下文对象(如 Activity、Application,此处为 MainActivity)
+ */
+ public void openWebsiteInBrowser(Context context) {
+ String url = "https://www.winboll.cc"; // 目标网站 URL(应用官网)
+ // 构建隐式意图:ACTION_VIEW 表示查看指定数据(Uri 为网站地址)
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ // 设置标志:在新的任务栈中启动 Activity(避免与当前应用任务栈混淆)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // 启动意图(唤起浏览器)
+ context.startActivity(intent);
+ }
}
+
diff --git a/appbase/src/main/res/layout/activity_main.xml b/appbase/src/main/res/layout/activity_main.xml
index 9552e3ad..5ba647d7 100644
--- a/appbase/src/main/res/layout/activity_main.xml
+++ b/appbase/src/main/res/layout/activity_main.xml
@@ -11,37 +11,57 @@
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
-
+ android:layout_weight="1.0">
-
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:spacing="12dp">
-
+
-
+
+
+
+
+
+
+
diff --git a/appbase/src/main/res/values/strings.xml b/appbase/src/main/res/values/strings.xml
index fdd16ce7..f32dc0a8 100644
--- a/appbase/src/main/res/values/strings.xml
+++ b/appbase/src/main/res/values/strings.xml
@@ -1,5 +1,4 @@
AppBase
- WinBoLL
diff --git a/libappbase/build.gradle b/libappbase/build.gradle
index c5346fe4..37c0ea42 100644
--- a/libappbase/build.gradle
+++ b/libappbase/build.gradle
@@ -5,11 +5,14 @@ apply from: '../.winboll/winboll_lint_build.gradle'
android {
- compileSdkVersion 32
- buildToolsVersion "32.0.0"
+ // 1. compileSdkVersion:必须 ≥ targetSdkVersion,建议直接等于 targetSdkVersion(30)
+ compileSdkVersion 30
+
+ // 2. buildToolsVersion:需匹配 compileSdkVersion,建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion)
+ buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
defaultConfig {
- minSdkVersion 24
+ minSdkVersion 23
targetSdkVersion 30
}
buildTypes {
@@ -22,9 +25,4 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
- // 网络连接类库
- api 'com.squareup.okhttp3:okhttp:4.4.1'
- // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
-
- api 'com.google.code.gson:gson:2.10.1'
}
diff --git a/libappbase/build.properties b/libappbase/build.properties
index 8e51c0ba..5d5b92ed 100644
--- a/libappbase/build.properties
+++ b/libappbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Fri Sep 26 05:36:14 HKT 2025
-stageCount=9
+#Tue Nov 18 07:02:48 GMT 2025
+stageCount=1
libraryProject=libappbase
-baseVersion=15.10
-publishVersion=15.10.8
-buildCount=0
-baseBetaVersion=15.10.9
+baseVersion=15.11
+publishVersion=15.11.0
+buildCount=7
+baseBetaVersion=15.11.1
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java
index 8d934696..d4dc9ca2 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java
@@ -1,73 +1,138 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/03/02 10:28:08
- * @Describe 应用调试模型
- */
import android.util.JsonReader;
import android.util.JsonWriter;
-import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:01
+ * @Describe WinBoLL 应用全局数据模型类
+ * 继承自 BaseBean,用于存储和管理应用的核心配置信息(如调试状态),
+ * 支持 JSON 序列化/反序列化,便于数据持久化或跨组件传递
+ */
public class APPModel extends BaseBean {
+ /**
+ * 日志打印标签,用于区分当前类的日志输出
+ */
public static final String TAG = "APPModel";
- // 应用是否处于正在调试状态
- //
- boolean isDebuging = false;
+ /**
+ * 应用调试状态标识
+ * true:应用处于调试模式(可输出详细日志、启用调试功能等)
+ * false:应用处于正式模式(关闭调试相关功能,优化性能)
+ */
+ private boolean isDebugging = false; // 修正拼写:原 isDebuging -> isDebugging(符合命名规范)
+ /**
+ * 无参构造方法
+ * 初始化调试状态为默认值:false(正式模式)
+ */
public APPModel() {
- this.isDebuging = false;
+ this.isDebugging = false;
}
- public APPModel(boolean isDebuging) {
- this.isDebuging = isDebuging;
+ /**
+ * 带参构造方法
+ * 可通过参数指定应用的初始调试状态
+ * @param isDebugging 初始调试状态(true:调试模式;false:正式模式)
+ */
+ public APPModel(boolean isDebugging) {
+ this.isDebugging = isDebugging;
}
- public void setIsDebuging(boolean isDebuging) {
- this.isDebuging = isDebuging;
+ /**
+ * 设置应用调试状态
+ * @param isDebugging 目标调试状态(true:开启调试;false:关闭调试)
+ */
+ public void setIsDebugging(boolean isDebugging) {
+ this.isDebugging = isDebugging;
}
- public boolean isDebuging() {
- return isDebuging;
+ /**
+ * 获取当前应用调试状态
+ * @return 调试状态(true:调试中;false:非调试)
+ */
+ public boolean isDebugging() {
+ return isDebugging;
}
+ /**
+ * 重写父类方法,返回当前类的全限定名
+ * 用于标识数据模型的类类型(可用于反射、序列化校验等场景)
+ * @return 类的全限定名(如:cc.winboll.studio.libappbase.APPModel)
+ */
@Override
public String getName() {
return APPModel.class.getName();
}
+ /**
+ * 重写父类方法,将当前模型的字段序列化到 JSON 中
+ * 用于将调试状态等核心数据转换为 JSON 格式(如持久化到文件、网络传输)
+ * @param jsonWriter JSON 写入器对象,用于输出 JSON 数据
+ * @throws IOException 当 JSON 写入失败时抛出(如流关闭、格式错误)
+ */
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ // 先调用父类方法,序列化父类中的字段(若 BaseBean 有可序列化字段)
super.writeThisToJsonWriter(jsonWriter);
- jsonWriter.name("isDebuging").value(isDebuging());
+ // 序列化当前类的调试状态字段:key 为 "isDebuging"(保持与原代码一致,避免兼容性问题),value 为当前状态
+ jsonWriter.name("isDebuging").value(isDebugging());
}
+ /**
+ * 重写父类方法,从 JSON 中解析字段并初始化当前对象
+ * 用于将 JSON 格式的配置数据解析为 APPModel 实例(如从文件读取、网络接收后解析)
+ * @param jsonReader JSON 读取器对象,用于读取 JSON 数据
+ * @param name 当前解析的 JSON 字段名
+ * @return true:字段解析成功;false:字段不属于当前类,需由调用者处理
+ * @throws IOException 当 JSON 读取失败时抛出(如流关闭、数据格式错误)
+ */
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
- if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ // 先调用父类方法,解析父类中的字段(若 BaseBean 有可解析字段)
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true; // 父类已处理该字段,直接返回成功
+ } else {
+ // 解析当前类的字段
if (name.equals("isDebuging")) {
- setIsDebuging(jsonReader.nextBoolean());
+ // 读取 JSON 中 "isDebuging" 字段的值,设置为当前对象的调试状态
+ setIsDebugging(jsonReader.nextBoolean());
} else {
+ // 字段不属于当前类,返回 false 提示调用者跳过该字段
return false;
}
}
+ // 字段解析成功,返回 true
return true;
}
+ /**
+ * 重写父类方法,从 JSON 读取器中完整解析一个 APPModel 实例
+ * 负责处理 JSON 对象的开始/结束标记,循环解析所有字段
+ * @param jsonReader JSON 读取器对象,用于读取 JSON 数据
+ * @return 解析完成的当前 APPModel 实例(支持链式调用)
+ * @throws IOException 当 JSON 读取失败时抛出(如流关闭、数据格式错误)
+ */
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ // 开始解析 JSON 对象(对应 JSON 中的 '{')
jsonReader.beginObject();
+ // 循环读取 JSON 中的所有字段(直到对象结束)
while (jsonReader.hasNext()) {
+ // 获取当前字段名
String name = jsonReader.nextName();
+ // 解析字段:若当前类无法处理该字段,则跳过(避免解析异常)
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue();
}
}
- // 结束 JSON 对象
+ // 结束解析 JSON 对象(对应 JSON 中的 '}')
jsonReader.endObject();
+ // 返回解析完成的实例(当前对象)
return this;
}
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java
index e1b5e7b3..372e6ef2 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/01/15 11:11:52
- * @Describe Json Bean 基础类。
- */
import android.content.Context;
import android.util.JsonReader;
import android.util.JsonWriter;
@@ -14,274 +9,428 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:03
+ * @Describe WinBoLL JSON 数据模型基类(抽象类)
+ * 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑,
+ * 子类(如 APPModel)需实现抽象方法,实现自身字段的 JSON 读写
+ * @param 泛型约束,限定子类必须继承自 BaseBean
+ */
public abstract class BaseBean {
- public static final String TAG = "BaseBean";
- static final String BEAN_NAME = "BeanName";
+ /** 日志标签,用于当前基类的日志输出标识 */
+ public static final String TAG = "BaseBean";
+ /** JSON 中存储 Bean 类名的字段键(用于校验 Bean 类型一致性) */
+ static final String BEAN_NAME = "BeanName";
- public BaseBean() {}
+ /**
+ * 无参构造方法(子类需默认实现,支持反射实例化)
+ */
+ public BaseBean() {}
- public abstract String getName();
+ /**
+ * 抽象方法:获取当前 Bean 的全限定类名
+ * 子类需实现,用于标识 Bean 类型(序列化/校验时使用)
+ * @return 类的全限定名(如:cc.winboll.studio.libappbase.APPModel)
+ */
+ public abstract String getName();
- public String getBeanJsonFilePath(Context context) {
+ /**
+ * 获取单个 Bean 的 JSON 持久化文件路径
+ * 路径:外部存储/应用私有目录/BaseBean/[类名].json
+ * @param context 上下文(用于获取应用存储目录)
+ * @return 单个 Bean 的文件绝对路径
+ */
+ public String getBeanJsonFilePath(Context context) {
+ return context.getExternalFilesDir(TAG) + "/" + getName() + ".json";
+ }
- return context.getExternalFilesDir(TAG) + "/" + getName() + ".json";
- }
+ /**
+ * 获取 Bean 列表的 JSON 持久化文件路径
+ * 路径:外部存储/应用私有目录/BaseBean/[类名]_List.json
+ * @param context 上下文(用于获取应用存储目录)
+ * @return Bean 列表的文件绝对路径
+ */
+ public String getBeanListJsonFilePath(Context context) {
+ return context.getExternalFilesDir(TAG) + "/" + getName() + "_List.json";
+ }
- public String getBeanListJsonFilePath(Context context) {
+ /**
+ * 将 Bean 类名写入 JSON(序列化基础字段)
+ * 子类可重写扩展,添加自身字段的 JSON 写入逻辑
+ * @param jsonWriter JSON 写入器(用于输出 JSON 数据)
+ * @throws IOException JSON 写入失败时抛出(如流异常)
+ */
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ // 写入 Bean 类名字段(用于反序列化时校验类型)
+ jsonWriter.name(BEAN_NAME).value(getName());
+ }
- return context.getExternalFilesDir(TAG) + "/" + getName() + "_List.json";
- }
+ /**
+ * 从 JSON 读取字段并初始化 Bean(反序列化基础逻辑)
+ * 子类需重写,实现自身字段的解析逻辑
+ * @param jsonReader JSON 读取器(用于读取 JSON 数据)
+ * @param name 当前解析的 JSON 字段名
+ * @return true:字段解析成功(当前类处理);false:字段未处理(需跳过)
+ * @throws IOException JSON 读取失败时抛出(如流异常)
+ */
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ return false; // 基类未处理任何字段,返回 false
+ }
- public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
- jsonWriter.name(BEAN_NAME).value(getName());
- }
+ /**
+ * 抽象方法:从 JSON 读取器解析并返回 Bean 实例
+ * 子类需实现,处理自身字段的完整解析逻辑
+ * @param jsonReader JSON 读取器(用于读取 JSON 数据)
+ * @return 解析完成的 Bean 实例
+ * @throws IOException JSON 读取失败时抛出(如流异常)
+ */
+ abstract public T readBeanFromJsonReader(JsonReader jsonReader) throws IOException;
- public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
- return false;
- }
+ /**
+ * 校验 JSON 文件中的 Bean 列表与目标类是否一致
+ * 对比文件中每个 Bean 的类名与目标类名,返回不一致信息
+ * @param szFilePath JSON 文件路径(存储 Bean 列表的文件)
+ * @param clazz 目标 Bean 类(用于校验类型)
+ * @return 空串:校验一致;非空串:不一致信息(总数/差异数)或异常信息
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static String checkIsTheSameBeanListAndFile(String szFilePath, Class clazz) {
+ StringBuilder sbResult = new StringBuilder();
+ String szErrorInfo = "Check Is The Same Bean List And File Error : ";
- abstract public T readBeanFromJsonReader(JsonReader jsonReader) throws IOException;
+ try {
+ int sameCount = 0; // 类名匹配的 Bean 数量
+ int totalCount = 0; // 文件中 Bean 总数量
- public static String checkIsTheSameBeanListAndFile(String szFilePath, Class clazz) {
- StringBuilder sbResult = new StringBuilder();
- String szErrorInfo = "Check Is The Same Bean List And File Error : ";
+ // 反射创建目标 Bean 实例(用于获取类名)
+ T beanTemp = clazz.newInstance();
+ String targetBeanName = beanTemp.getName();
+ // 读取文件中的 JSON 字符串
+ String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
+ StringReader stringReader = new StringReader(listJson);
+ JsonReader jsonReader = new JsonReader(stringReader);
- try {
- int nSameCount = 0;
- int nBeanListCout = 0;
+ jsonReader.beginArray(); // 开始解析 JSON 数组(Bean 列表)
+ while (jsonReader.hasNext()) {
+ totalCount++;
+ jsonReader.beginObject(); // 开始解析单个 Bean 对象
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ // 只校验 BEAN_NAME 字段,其他字段跳过
+ if (name.equals(BEAN_NAME)) {
+ // 对比当前 Bean 类名与目标类名
+ if (targetBeanName.equals(jsonReader.nextString())) {
+ sameCount++;
+ }
+ } else {
+ jsonReader.skipValue(); // 跳过非目标字段
+ }
+ }
+ jsonReader.endObject(); // 结束单个 Bean 对象解析
+ }
+ jsonReader.endArray(); // 结束 JSON 数组解析
- T beanTemp = clazz.newInstance();
- String szBeanSimpleName = beanTemp.getName();
- String szListJson = UTF8FileUtils.readStringFromFile(szFilePath);
- StringReader stringReader = new StringReader(szListJson);
- JsonReader jsonReader = new JsonReader(stringReader);
- jsonReader.beginArray();
- while (jsonReader.hasNext()) {
- nBeanListCout++;
- jsonReader.beginObject();
- while (jsonReader.hasNext()) {
- String name = jsonReader.nextName();
- if (name.equals(BEAN_NAME)) {
- if (szBeanSimpleName.equals(jsonReader.nextString())) {
- nSameCount++;
- }
- } else {
- jsonReader.skipValue();
- }
- }
- jsonReader.endObject();
- }
- jsonReader.endArray();
+ // 生成校验结果
+ if (sameCount == totalCount) {
+ return ""; // 全部匹配,返回空串
+ } else {
+ // 部分不匹配,返回统计信息
+ sbResult.append("Total : ").append(totalCount)
+ .append(" Diff : ").append(totalCount - sameCount);
+ }
+ } catch (InstantiationException e) {
+ // 反射实例化失败(如无无参构造)
+ sbResult.append(szErrorInfo).append(e);
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ } catch (IllegalAccessException e) {
+ // 反射访问权限异常
+ sbResult.append(szErrorInfo).append(e);
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ } catch (IOException e) {
+ // 文件读取或 JSON 解析异常
+ sbResult.append(szErrorInfo).append(e);
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return sbResult.toString();
+ }
- // 返回检查结果
- if (nSameCount == nBeanListCout) {
- // 检查一致直接返回空串
- return "";
- } else {
- // 检查不一致返回对比信息
- sbResult.append("Total : ");
- sbResult.append(nBeanListCout);
- sbResult.append(" Diff : ");
- sbResult.append(nBeanListCout - nSameCount);
- }
- } catch (InstantiationException e) {
- sbResult.append(szErrorInfo);
- sbResult.append(e);
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- sbResult.append(szErrorInfo);
- sbResult.append(e);
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IOException e) {
- sbResult.append(szErrorInfo);
- sbResult.append(e);
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return sbResult.toString();
- }
+ /**
+ * 将 JSON 字符串解析为目标 Bean 实例
+ * 通过反射创建 Bean 实例,调用子类解析逻辑完成初始化
+ * @param szBean JSON 字符串(单个 Bean 的 JSON 数据)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return 解析成功的 Bean 实例;失败返回 null
+ * @throws IOException JSON 解析失败时抛出
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static T parseStringToBean(String szBean, Class clazz) throws IOException {
+ StringReader stringReader = new StringReader(szBean);
+ JsonReader jsonReader = new JsonReader(stringReader);
- public static T parseStringToBean(String szBean, Class clazz) throws IOException {
- // 创建 JsonWriter 对象
- StringReader stringReader = new StringReader(szBean);
- JsonReader jsonReader = new JsonReader(stringReader);
- try {
- T beanTemp = clazz.newInstance();
- return (T)beanTemp.readBeanFromJsonReader(jsonReader);
- } catch (InstantiationException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return null;
- }
+ try {
+ // 反射创建 Bean 实例
+ T beanTemp = clazz.newInstance();
+ // 调用子类解析方法,返回解析后的实例
+ return (T) beanTemp.readBeanFromJsonReader(jsonReader);
+ } catch (InstantiationException | IllegalAccessException e) {
+ // 反射异常日志记录
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return null;
+ }
- public static boolean parseStringToBeanList(String szBeanList, ArrayList beanList, Class clazz) {
- try {
- if(beanList == null) {
- beanList = new ArrayList();
- } else {
- beanList.clear();
- }
- StringReader stringReader = new StringReader(szBeanList);
- JsonReader jsonReader = new JsonReader(stringReader);
- jsonReader.beginArray();
- while (jsonReader.hasNext()) {
- T beanTemp = clazz.newInstance();
- T bean = (T)beanTemp.readBeanFromJsonReader(jsonReader);
- if (bean != null) {
- beanList.add(bean);
- //LogUtils.d(TAG, "beanList.add(bean)");
- }
- }
- jsonReader.endArray();
- return true;
- //LogUtils.d(TAG, "beanList.size() is " + Integer.toString(beanList.size()));
- } catch (InstantiationException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 将 JSON 字符串解析为 Bean 列表
+ * 清空目标列表,将解析后的 Bean 逐个添加到列表中
+ * @param szBeanList JSON 字符串(Bean 列表的 JSON 数组)
+ * @param beanList 目标列表(存储解析后的 Bean)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return true:解析成功;false:解析失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean parseStringToBeanList(String szBeanList, ArrayList beanList, Class clazz) {
+ try {
+ // 初始化目标列表(为空则创建,非空则清空)
+ if (beanList == null) {
+ beanList = new ArrayList();
+ } else {
+ beanList.clear();
+ }
- @Override
- public String toString() {
- // 创建 JsonWriter 对象
- StringWriter stringWriter = new StringWriter();
- JsonWriter jsonWriter = new JsonWriter(stringWriter);
- jsonWriter.setIndent(" ");
- try {// 开始 JSON 对象
- jsonWriter.beginObject();
- // 写入键值对
- writeThisToJsonWriter(jsonWriter);
- // 结束 JSON 对象
- jsonWriter.endObject();
- return stringWriter.toString();
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- // 获取 JSON 字符串
- return "";
- }
+ StringReader stringReader = new StringReader(szBeanList);
+ JsonReader jsonReader = new JsonReader(stringReader);
- public static String toStringByBeanList(ArrayList beanList) {
- try {
- StringWriter stringWriter = new StringWriter();
- JsonWriter jsonWriter = new JsonWriter(stringWriter);
- jsonWriter.setIndent(" ");
- jsonWriter.beginArray();
- for (int i = 0; i < beanList.size(); i++) {
- // 开始 JSON 对象
- jsonWriter.beginObject();
- // 写入键值对
- beanList.get(i).writeThisToJsonWriter(jsonWriter);
- // 结束 JSON 对象
- jsonWriter.endObject();
- }
- jsonWriter.endArray();
- jsonWriter.close();
- return stringWriter.toString();
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return "";
- }
+ jsonReader.beginArray(); // 开始解析 JSON 数组
+ while (jsonReader.hasNext()) {
+ // 反射创建 Bean 实例,解析并添加到列表
+ T beanTemp = clazz.newInstance();
+ T bean = (T) beanTemp.readBeanFromJsonReader(jsonReader);
+ if (bean != null) {
+ beanList.add(bean);
+ }
+ }
+ jsonReader.endArray(); // 结束 JSON 数组解析
+ return true;
+ } catch (InstantiationException | IllegalAccessException | IOException e) {
+ // 异常日志记录
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
+ /**
+ * 重写 toString(),将 Bean 序列化为格式化的 JSON 字符串
+ * 调用自身序列化逻辑,生成带缩进的 JSON(便于调试)
+ * @return Bean 的 JSON 字符串;失败返回空串
+ */
+ @Override
+ public String toString() {
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" "); // 设置 JSON 缩进(格式化输出)
- public static T loadBean(Context context, Class clazz) {
- try {
- T beanTemp = clazz.newInstance();
- return loadBeanFromFile(beanTemp.getBeanJsonFilePath(context), clazz);
- } catch (InstantiationException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return null;
- }
+ try {
+ jsonWriter.beginObject(); // 开始 JSON 对象
+ writeThisToJsonWriter(jsonWriter); // 写入 Bean 字段(子类扩展)
+ jsonWriter.endObject(); // 结束 JSON 对象
+ return stringWriter.toString();
+ } catch (IOException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return "";
+ }
- public static T loadBeanFromFile(String szFilePath, Class clazz) {
- try {
- try {
- File fTemp = new File(szFilePath);
- if (fTemp.exists()) {
- T beanTemp = clazz.newInstance();String szJson = UTF8FileUtils.readStringFromFile(szFilePath);
- return beanTemp.parseStringToBean(szJson, clazz);
- }
- } catch (InstantiationException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
+ /**
+ * 将 Bean 列表序列化为格式化的 JSON 字符串
+ * 遍历列表,逐个序列化每个 Bean,生成 JSON 数组
+ * @param beanList 待序列化的 Bean 列表
+ * @return 列表的 JSON 字符串;失败返回空串
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static String toStringByBeanList(ArrayList beanList) {
+ try {
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" "); // 格式化缩进
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return null;
- }
+ jsonWriter.beginArray(); // 开始 JSON 数组
+ for (int i = 0; i < beanList.size(); i++) {
+ jsonWriter.beginObject(); // 单个 Bean 开始
+ beanList.get(i).writeThisToJsonWriter(jsonWriter); // 调用 Bean 自身序列化
+ jsonWriter.endObject(); // 单个 Bean 结束
+ }
+ jsonWriter.endArray(); // 结束 JSON 数组
+ jsonWriter.close();
+ return stringWriter.toString();
+ } catch (IOException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return "";
+ }
- public static boolean saveBean(Context context, T bean) {
- return saveBeanToFile(bean.getBeanJsonFilePath(context), bean);
- }
+ /**
+ * 从默认路径(getBeanJsonFilePath)加载 Bean 实例
+ * 读取应用私有目录下的 JSON 文件,解析为目标 Bean
+ * @param context 上下文(用于获取文件路径)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return 加载成功的 Bean 实例;失败返回 null
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static T loadBean(Context context, Class clazz) {
+ try {
+ // 反射创建 Bean 实例,获取默认文件路径
+ T beanTemp = clazz.newInstance();
+ return loadBeanFromFile(beanTemp.getBeanJsonFilePath(context), clazz);
+ } catch (InstantiationException | IllegalAccessException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return null;
+ }
- public static boolean saveBeanToFile(String szFilePath, T bean) {
- try {
- String szJson = bean.toString();
- UTF8FileUtils.writeStringToFile(szFilePath, szJson);
- return true;
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 从指定文件路径加载 Bean 实例
+ * 检查文件是否存在,存在则读取 JSON 并解析为目标 Bean
+ * @param szFilePath 目标文件路径(存储 Bean 的 JSON 文件)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return 加载成功的 Bean 实例;失败返回 null
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static T loadBeanFromFile(String szFilePath, Class clazz) {
+ try {
+ File file = new File(szFilePath);
+ if (file.exists()) { // 检查文件是否存在
+ T beanTemp = clazz.newInstance();
+ // 读取文件 JSON 字符串,解析为 Bean
+ String json = UTF8FileUtils.readStringFromFile(szFilePath);
+ return beanTemp.parseStringToBean(json, clazz);
+ }
+ } catch (InstantiationException | IllegalAccessException | IOException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return null;
+ }
- public static boolean loadBeanList(Context context, ArrayList beanListDst, Class clazz) {
- try {
- T beanTemp = clazz.newInstance();
- return loadBeanListFromFile(beanTemp.getBeanListJsonFilePath(context), beanListDst, clazz);
- } catch (InstantiationException e) {} catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 将 Bean 保存到默认路径(getBeanJsonFilePath)的文件中
+ * 序列化 Bean 为 JSON,写入应用私有目录下的文件
+ * @param context 上下文(用于获取文件路径)
+ * @param bean 待保存的 Bean 实例
+ * @return true:保存成功;false:保存失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean saveBean(Context context, T bean) {
+ return saveBeanToFile(bean.getBeanJsonFilePath(context), bean);
+ }
- public static boolean loadBeanListFromFile(String szFilePath, ArrayList beanList, Class clazz) {
- try {
- File fTemp = new File(szFilePath);
- if (fTemp.exists()) {
- String szListJson = UTF8FileUtils.readStringFromFile(szFilePath);
- return parseStringToBeanList(szListJson, beanList, clazz);
- }
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 将 Bean 保存到指定文件路径
+ * 序列化 Bean 为 JSON 字符串,写入目标文件(覆盖原有内容)
+ * @param szFilePath 目标文件路径(保存 JSON 的文件)
+ * @param bean 待保存的 Bean 实例
+ * @return true:保存成功;false:保存失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean saveBeanToFile(String szFilePath, T bean) {
+ try {
+ // 序列化 Bean 为 JSON 字符串
+ String json = bean.toString();
+ // 写入文件(UTF-8 编码)
+ UTF8FileUtils.writeStringToFile(szFilePath, json);
+ return true;
+ } catch (IOException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
- public static boolean saveBeanList(Context context, ArrayList beanList, Class clazz) {
- try {
- T beanTemp = clazz.newInstance();
- return saveBeanListToFile(beanTemp.getBeanListJsonFilePath(context), beanList);
- } catch (InstantiationException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- } catch (IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 从默认路径(getBeanListJsonFilePath)加载 Bean 列表
+ * 读取应用私有目录下的列表 JSON 文件,解析并填充到目标列表
+ * @param context 上下文(用于获取文件路径)
+ * @param beanListDst 目标列表(存储加载后的 Bean)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return true:加载成功;false:加载失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean loadBeanList(Context context, ArrayList beanListDst, Class clazz) {
+ try {
+ // 反射创建 Bean 实例,获取默认列表文件路径
+ T beanTemp = clazz.newInstance();
+ return loadBeanListFromFile(beanTemp.getBeanListJsonFilePath(context), beanListDst, clazz);
+ } catch (InstantiationException | IllegalAccessException e) {
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
- public static boolean saveBeanListToFile(String szFilePath, ArrayList beanList) {
- try {
- String szJson = toStringByBeanList(beanList);
- UTF8FileUtils.writeStringToFile(szFilePath, szJson);
- //LogUtils.d(TAG, "FileUtil.writeFile beanList.size() is " + Integer.toString(beanList.size()));
- return true;
- } catch (IOException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- }
- return false;
- }
+ /**
+ * 从指定文件路径加载 Bean 列表
+ * 检查文件是否存在,存在则读取 JSON 数组,解析并填充到目标列表
+ * @param szFilePath 目标文件路径(存储列表 JSON 的文件)
+ * @param beanList 目标列表(存储加载后的 Bean)
+ * @param clazz 目标 Bean 类(用于反射实例化)
+ * @return true:加载成功;false:加载失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean loadBeanListFromFile(String szFilePath, ArrayList beanList, Class clazz) {
+ try {
+ File file = new File(szFilePath);
+ if (file.exists()) { // 检查文件是否存在
+ // 读取文件中的 JSON 字符串(Bean 列表数组)
+ String listJson = UTF8FileUtils.readStringFromFile(szFilePath);
+ // 解析 JSON 字符串为 Bean 列表,填充到目标列表
+ return parseStringToBeanList(listJson, beanList, clazz);
+ }
+ } catch (IOException e) {
+ // 日志记录文件读取或解析异常
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
+
+ /**
+ * 将 Bean 列表保存到默认路径(getBeanListJsonFilePath)的文件中
+ * 序列化列表为 JSON 数组,写入应用私有目录下的文件
+ * @param context 上下文(用于获取文件路径)
+ * @param beanList 待保存的 Bean 列表
+ * @param clazz 目标 Bean 类(用于反射获取保存路径)
+ * @return true:保存成功;false:保存失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean saveBeanList(Context context, ArrayList beanList, Class clazz) {
+ try {
+ // 反射创建 Bean 实例,获取默认列表保存路径
+ T beanTemp = clazz.newInstance();
+ return saveBeanListToFile(beanTemp.getBeanListJsonFilePath(context), beanList);
+ } catch (InstantiationException | IllegalAccessException e) {
+ // 日志记录反射实例化异常
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
+
+ /**
+ * 将 Bean 列表保存到指定文件路径
+ * 序列化列表为 JSON 数组字符串,写入目标文件(覆盖原有内容)
+ * @param szFilePath 目标文件路径(保存列表 JSON 的文件)
+ * @param beanList 待保存的 Bean 列表
+ * @return true:保存成功;false:保存失败
+ * @param 泛型约束,限定为 BaseBean 子类
+ */
+ public static boolean saveBeanListToFile(String szFilePath, ArrayList beanList) {
+ try {
+ // 序列化 Bean 列表为 JSON 字符串(数组格式)
+ String json = toStringByBeanList(beanList);
+ // 将 JSON 字符串写入文件(UTF-8 编码)
+ UTF8FileUtils.writeStringToFile(szFilePath, json);
+ return true;
+ } catch (IOException e) {
+ // 日志记录文件写入或序列化异常
+ LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
+ }
+ return false;
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java
index 5b97af6c..2432cd9b 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2024/08/12 13:22:12
- * @Describe 异常处理类
- */
import android.app.Activity;
import android.app.Application;
import android.content.ActivityNotFoundException;
@@ -12,29 +7,22 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
-import android.text.SpannableString;
import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
-import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
-import android.widget.Toolbar;
-import cc.winboll.studio.libappbase.R;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -48,358 +36,515 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:14
+ * @Describe * 应用全局崩溃处理类(单例逻辑)
+ * 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
+ * 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
+ */
public final class CrashHandler {
- public static final String TAG = "CrashHandler";
+ /** 日志标签,用于当前类的日志输出标识 */
+ public static final String TAG = "CrashHandler";
- public static final String TITTLE = "CrashReport";
+ /** 崩溃报告页面标题 */
+ public static final String TITTLE = "CrashReport";
- public static final String EXTRA_CRASH_INFO = "crashInfo";
+ /** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
+ public static final String EXTRA_CRASH_INFO = "crashInfo";
- final static String PREFS = CrashHandler.class.getName() + "PREFS";
- final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
+ /** SharedPreferences 存储键(用于记录崩溃状态) */
+ final static String PREFS = CrashHandler.class.getName() + "PREFS";
+ /** SharedPreferences 中存储「是否发生崩溃」的键 */
+ final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
- public static String _CrashCountFilePath;
+ /** 崩溃保险丝状态文件路径(存储当前熔断等级) */
+ public static String _CrashCountFilePath;
- public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
+ /** 系统默认的未捕获异常处理器(用于降级处理,避免 CrashHandler 自身崩溃) */
+ public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
- public static void init(Application app) {
- _CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
- LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
- init(app, null);
- }
+ /**
+ * 初始化崩溃处理器(默认存储路径)
+ * 调用重载方法,崩溃日志默认存储在应用外部私有目录的 crash 文件夹下
+ * @param app 全局 Application 实例(用于获取存储目录、包信息等)
+ */
+ public static void init(Application app) {
+ // 初始化崩溃保险丝状态文件路径(外部存储/CrashHandler/IsCrashHandlerCrashHappen.dat)
+ _CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
+ LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
+ // 调用带目录参数的初始化方法,传入 null 使用默认路径
+ init(app, null);
+ }
- public static void init(final Application app, final String crashDir) {
- Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(){
+ /**
+ * 初始化崩溃处理器(指定日志存储目录)
+ * 替换系统默认的未捕获异常处理器,自定义崩溃处理逻辑
+ * @param app 全局 Application 实例
+ * @param crashDir 崩溃日志存储目录(null 则使用默认路径)
+ */
+ public static void init(final Application app, final String crashDir) {
+ // 设置自定义未捕获异常处理器
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ try {
+ // 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
+ tryUncaughtException(thread, throwable);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ // 处理失败时,交给系统默认处理器兜底
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
+ DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ }
+ }
- @Override
- public void uncaughtException(Thread thread, Throwable throwable) {
- try {
- tryUncaughtException(thread, throwable);
- } catch (Throwable e) {
- e.printStackTrace();
- if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null)
- DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
- }
- }
+ /**
+ * 实际处理崩溃的核心方法
+ * 1. 熔断保险丝(记录崩溃次数);2. 收集崩溃信息;3. 写入日志文件;4. 启动崩溃报告页面
+ * @param thread 发生崩溃的线程
+ * @param throwable 崩溃异常对象(包含堆栈信息)
+ */
+ private void tryUncaughtException(Thread thread, Throwable throwable) {
+ // 触发崩溃保险丝(每次崩溃熔断一次,降低防护等级)
+ AppCrashSafetyWire.getInstance().burnSafetyWire();
- private void tryUncaughtException(Thread thread, Throwable throwable) {
- // 每到这里就燃烧一次保险丝
- AppCrashSafetyWire.getInstance().burnSafetyWire();
+ // 格式化崩溃发生时间(用于日志文件名和内容)
+ final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
+ // 创建崩溃日志文件(默认路径:外部存储/crash/[时间].txt)
+ File crashFile = new File(
+ TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash") : new File(crashDir),
+ "crash_" + time + ".txt"
+ );
- final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
- File crashFile = new File(TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash")
- : new File(crashDir), "crash_" + time + ".txt");
+ // 获取应用版本信息(版本名、版本号)
+ String versionName = "unknown";
+ long versionCode = 0;
+ try {
+ PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
+ versionName = packageInfo.versionName;
+ // 适配 Android 9.0+(API 28)的版本号获取方式
+ versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
+ } catch (PackageManager.NameNotFoundException ignored) {}
- String versionName = "unknown";
- long versionCode = 0;
- try {
- PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
- versionName = packageInfo.versionName;
- versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode()
- : packageInfo.versionCode;
- } catch (PackageManager.NameNotFoundException ignored) {}
+ // 将异常堆栈信息转换为字符串
+ String fullStackTrace;
+ {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw); // 将异常堆栈写入 PrintWriter
+ fullStackTrace = sw.toString();
+ pw.close();
+ }
- String fullStackTrace; {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- throwable.printStackTrace(pw);
- fullStackTrace = sw.toString();
- pw.close();
- }
+ // 拼接崩溃信息(设备信息 + 应用信息 + 堆栈信息)
+ StringBuilder sb = new StringBuilder();
+ sb.append("************* Crash Head ****************\n");
+ sb.append("Time Of Crash : ").append(time).append("\n");
+ sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n"); // 设备厂商
+ sb.append("Device Model : ").append(Build.MODEL).append("\n"); // 设备型号
+ sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n"); // Android 版本
+ sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n"); // SDK 版本
+ sb.append("App VersionName : ").append(versionName).append("\n"); // 应用版本名
+ sb.append("App VersionCode : ").append(versionCode).append("\n"); // 应用版本号
+ sb.append("************* Crash Head ****************\n");
+ sb.append("\n").append(fullStackTrace); // 拼接异常堆栈
- StringBuilder sb = new StringBuilder();
- sb.append("************* Crash Head ****************\n");
- sb.append("Time Of Crash : ").append(time).append("\n");
- sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n");
- sb.append("Device Model : ").append(Build.MODEL).append("\n");
- sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n");
- sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n");
- sb.append("App VersionName : ").append(versionName).append("\n");
- sb.append("App VersionCode : ").append(versionCode).append("\n");
- sb.append("************* Crash Head ****************\n");
- sb.append("\n").append(fullStackTrace);
+ final String errorLog = sb.toString();
- String errorLog = sb.toString();
+ // 将崩溃日志写入文件(忽略写入失败)
+ try {
+ writeFile(crashFile, errorLog);
+ } catch (IOException ignored) {}
- try {
- writeFile(crashFile, errorLog);
- } catch (IOException ignored) {}
+ // 启动崩溃报告页面(标签用于代码块折叠)
+ gotoCrashActiviy: {
+ Intent intent = new Intent();
+ LogUtils.d(TAG, "gotoCrashActiviy: ");
- gotoCrashActiviy: {
- Intent intent = new Intent();
- LogUtils.d(TAG, "gotoCrashActiviy: ");
- if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
- LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
- intent.setClass(app, GlobalCrashActivity.class);
- intent.putExtra(EXTRA_CRASH_INFO, errorLog);
- // 如果发生了 CrashHandler 内部崩溃, 就调用基础的应用崩溃显示类
-// intent.setClass(app, GlobalCrashActiviy.class);
-// intent.putExtra(GlobalCrashActiviy.EXTRA_CRASH_INFO, errorLog);
- } else {
- LogUtils.d(TAG, "gotoCrashActiviy: else");
- // 正常状态调用进阶的应用崩溃显示页
- intent.setClass(app, CrashActivity.class);
- intent.putExtra(EXTRA_CRASH_INFO, errorLog);
- }
+ // 根据保险丝状态选择启动的崩溃页面
+ if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
+ LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
+ // 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
+ intent.setClass(app, GlobalCrashActivity.class);
+ intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志
+ } else {
+ LogUtils.d(TAG, "gotoCrashActiviy: else");
+ // 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
+ intent.setClass(app, CrashActivity.class);
+ intent.putExtra(EXTRA_CRASH_INFO, errorLog);
+ }
+ // 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ );
+ try {
+ // 启动崩溃页面,终止当前进程(确保完全重启)
+ app.startActivity(intent);
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+ } catch (ActivityNotFoundException e) {
+ // 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
+ e.printStackTrace();
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
+ DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ } catch (Exception e) {
+ // 其他异常,兜底处理
+ e.printStackTrace();
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
+ DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ }
+ }
+ }
-// intent.setClass(app, CrashActiviy.class);
-// intent.putExtra(CrashActiviy.EXTRA_CRASH_INFO, errorLog);
+ /**
+ * 将字符串内容写入文件(创建父目录、覆盖写入)
+ * @param file 目标文件(包含路径)
+ * @param content 待写入的内容(崩溃日志)
+ * @throws IOException 文件创建或写入失败时抛出
+ */
+ private void writeFile(File file, String content) throws IOException {
+ File parentFile = file.getParentFile();
+ // 父目录不存在则创建
+ if (parentFile != null && !parentFile.exists()) {
+ parentFile.mkdirs();
+ }
+ file.createNewFile(); // 创建文件
+ FileOutputStream fos = new FileOutputStream(file);
+ fos.write(content.getBytes()); // 写入内容(默认 UTF-8 编码)
+ try {
+ fos.close(); // 关闭流
+ } catch (IOException e) {}
+ }
+ });
+ }
+ /**
+ * 应用崩溃保险丝内部类(单例)
+ * 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略
+ * 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面
+ */
+ public static final class AppCrashSafetyWire {
- intent.addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK
- );
+ /** 单例实例(volatile 保证多线程可见性) */
+ private static volatile AppCrashSafetyWire _AppCrashSafetyWire;
- try {
- app.startActivity(intent);
- android.os.Process.killProcess(android.os.Process.myPid());
- System.exit(0);
- } catch (ActivityNotFoundException e) {
- e.printStackTrace();
- if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null)
- DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
- } catch (Exception e) {
- e.printStackTrace();
- if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null)
- DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
- }
- }
+ /** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */
+ private volatile Integer currentSafetyLevel;
+ /** 最低熔断等级(1,再崩溃则熔断) */
+ private static final int _MINI = 1;
+ /** 最高熔断等级(2,初始状态) */
+ private static final int _MAX = 2;
- }
+ /**
+ * 私有构造方法(单例模式,禁止外部实例化)
+ * 初始化时加载本地存储的熔断等级
+ */
+ private AppCrashSafetyWire() {
+ LogUtils.d(TAG, "AppCrashSafetyWire()");
+ currentSafetyLevel = loadCurrentSafetyLevel();
+ }
- private void writeFile(File file, String content) throws IOException {
- File parentFile = file.getParentFile();
- if (parentFile != null && !parentFile.exists()) {
- parentFile.mkdirs();
- }
- file.createNewFile();
- FileOutputStream fos = new FileOutputStream(file);
- fos.write(content.getBytes());
- try {
- fos.close();
- } catch (IOException e) {}
- }
+ /**
+ * 获取单例实例(双重检查锁定,线程安全)
+ * @return AppCrashSafetyWire 单例
+ */
+ public static synchronized AppCrashSafetyWire getInstance() {
+ if (_AppCrashSafetyWire == null) {
+ _AppCrashSafetyWire = new AppCrashSafetyWire();
+ }
+ return _AppCrashSafetyWire;
+ }
- });
- }
+ /**
+ * 设置当前熔断等级(内存中)
+ * @param currentSafetyLevel 目标等级(1~2)
+ */
+ public void setCurrentSafetyLevel(int currentSafetyLevel) {
+ this.currentSafetyLevel = currentSafetyLevel;
+ }
- //
- // 应用崩溃保险丝
- //
- public static final class AppCrashSafetyWire {
+ /**
+ * 获取当前熔断等级(内存中)
+ * @return 当前等级(1~2 或 null)
+ */
+ public int getCurrentSafetyLevel() {
+ return currentSafetyLevel;
+ }
- volatile static AppCrashSafetyWire _AppCrashSafetyWire;
+ /**
+ * 保存熔断等级到本地文件(持久化,重启应用生效)
+ * @param currentSafetyLevel 待保存的等级
+ */
+ public void saveCurrentSafetyLevel(int currentSafetyLevel) {
+ LogUtils.d(TAG, "saveCurrentSafetyLevel()");
+ this.currentSafetyLevel = currentSafetyLevel;
+ try {
+ // 序列化等级到文件(ObjectOutputStream 写入 int)
+ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath));
+ oos.writeInt(currentSafetyLevel);
+ oos.flush();
+ oos.close();
+ LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel));
+ } catch (IOException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
- volatile Integer currentSafetyLevel; // 熔断值,为 0 表示熔断了。
- private static final int _MINI = 1;
- private static final int _MAX = 2;
+ /**
+ * 从本地文件加载熔断等级(应用启动时初始化)
+ * @return 加载的等级(文件不存在则初始化为 MAX(2))
+ */
+ public int loadCurrentSafetyLevel() {
+ LogUtils.d(TAG, "loadCurrentSafetyLevel()");
+ try {
+ File f = new File(_CrashCountFilePath);
+ if (f.exists()) {
+ // 反序列化从文件读取等级
+ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath));
+ currentSafetyLevel = ois.readInt();
+ LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel));
+ } else {
+ // 文件不存在,初始化等级为最高(2)并保存
+ currentSafetyLevel = _MAX;
+ LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX));
+ saveCurrentSafetyLevel(currentSafetyLevel);
+ }
+ } catch (IOException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ return currentSafetyLevel;
+ }
- AppCrashSafetyWire() {
- LogUtils.d(TAG, "AppCrashSafetyWire()");
- currentSafetyLevel = loadCurrentSafetyLevel();
- }
+ /**
+ * 熔断保险丝(每次崩溃调用,降低防护等级)
+ * @return 熔断后是否仍在防护范围内(true:是;false:已熔断)
+ */
+ boolean burnSafetyWire() {
+ LogUtils.d(TAG, "burnSafetyWire()");
+ // 加载当前等级
+ int safeLevel = loadCurrentSafetyLevel();
+ // 若在防护范围内(1~2),等级-1 并保存
+ if (isSafetyWireWorking(safeLevel)) {
+ LogUtils.d(TAG, "burnSafetyWire() use");
+ saveCurrentSafetyLevel(safeLevel - 1);
+ // 返回熔断后的状态
+ return isSafetyWireWorking(safeLevel - 1);
+ }
+ return false;
+ }
- public static synchronized AppCrashSafetyWire getInstance() {
- if (_AppCrashSafetyWire == null) {
- _AppCrashSafetyWire = new AppCrashSafetyWire();
- }
- return _AppCrashSafetyWire;
- }
+ /**
+ * 检查熔断等级是否在有效范围内(1~2)
+ * @param safetyLevel 待检查的等级
+ * @return true:在范围内(防护有效);false:超出范围(已熔断)
+ */
+ boolean isSafetyWireWorking(int safetyLevel) {
+ LogUtils.d(TAG, "isSafetyWireOK()");
+ LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
- public void setCurrentSafetyLevel(int currentSafetyLevel) {
- this.currentSafetyLevel = currentSafetyLevel;
- }
+ if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
+ LogUtils.d(TAG, String.format("In Safety Level"));
+ return true;
+ }
+ LogUtils.d(TAG, String.format("Out of Safety Level"));
+ return false;
+ }
- public int getCurrentSafetyLevel() {
- return currentSafetyLevel;
- }
+ /**
+ * 立即恢复熔断等级到最高(2)
+ * 用于重启应用后重置防护状态
+ */
+ void resumeToMaximumImmediately() {
+ LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
+ AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
+ }
- public void saveCurrentSafetyLevel(int currentSafetyLevel) {
- LogUtils.d(TAG, "saveCurrentSafetyLevel()");
- this.currentSafetyLevel = currentSafetyLevel;
- try {
- ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath));
- oos.writeInt(currentSafetyLevel);
- oos.flush();
- oos.close();
- LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel));
- } catch (IOException e) {
- LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
- }
- }
+ /**
+ * 关闭防护(设置等级为最低(1))
+ * 下次崩溃直接熔断
+ */
+ void off() {
+ LogUtils.d(TAG, "off()");
+ saveCurrentSafetyLevel(_MINI);
+ }
- public int loadCurrentSafetyLevel() {
- LogUtils.d(TAG, "loadCurrentSafetyLevel()");
- try {
- File f = new File(_CrashCountFilePath);
- if (f.exists()) {
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath));
- currentSafetyLevel = ois.readInt();
- LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel));
- } else {
- currentSafetyLevel = _MAX;
- LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX));
- saveCurrentSafetyLevel(currentSafetyLevel);
- }
- } catch (IOException e) {
- LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
- }
- return currentSafetyLevel;
- }
+ /**
+ * 检查当前保险丝是否有效(防护未熔断)
+ * @return true:有效(等级 1~2);false:已熔断
+ */
+ boolean isAppCrashSafetyWireOK() {
+ LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
+ currentSafetyLevel = loadCurrentSafetyLevel();
+ return isSafetyWireWorking(currentSafetyLevel);
+ }
- boolean burnSafetyWire() {
- LogUtils.d(TAG, "burnSafetyWire()");
- // 崩溃计数进入崩溃保险值
- int safeLevel = loadCurrentSafetyLevel();
- if (isSafetyWireWorking(safeLevel)) {
- // 如果保险丝未熔断, 就增加一次熔断值
- LogUtils.d(TAG, "burnSafetyWire() use");
- saveCurrentSafetyLevel(safeLevel - 1);
- return isSafetyWireWorking(safeLevel - 1);
- }
- return false;
- }
+ /**
+ * 延迟恢复保险丝到最高等级(500ms 后)
+ * 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃
+ * @param context 上下文(用于获取主线程 Handler)
+ */
+ void postResumeCrashSafetyWireHandler(final Context context) {
+ // 主线程延迟 500ms 执行(避免页面启动时阻塞)
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ LogUtils.d(TAG, "Handler run()");
+ // 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级
+ if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) {
+ AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
+ LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级");
+ }
+ }
+ }, 500);
+ }
+ }
- boolean isSafetyWireWorking(int safetyLevel) {
- LogUtils.d(TAG, "isSafetyWireOK()");
- //safetyLevel = _MINI;
- //safetyLevel = _MINI - 1;
- //safetyLevel = _MINI + 1;
- //safetyLevel = _MAX;
- //safetyLevel = _MAX + 1;
- LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
+ /**
+ * 基础版崩溃报告页面(保险丝熔断时启动)
+ * 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
+ */
+ public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
+ /** 菜单标识:复制崩溃日志 */
+ private static final int MENUITEM_COPY = 0;
+ /** 菜单标识:重启应用 */
+ private static final int MENUITEM_RESTART = 1;
- if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
- // 如果在保险值之内
- LogUtils.d(TAG, String.format("In Safety Level"));
- return true;
- }
- LogUtils.d(TAG, String.format("Out of Safety Level"));
- return false;
- }
+ /** 崩溃日志文本(从 CrashHandler 传递过来) */
+ private String mLog;
- void resumeToMaximumImmediately() {
- LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
- AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
- }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // 初始化崩溃保险丝延迟恢复机制
+ AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
- void off() {
- LogUtils.d(TAG, "off()");
- saveCurrentSafetyLevel(_MINI);
- }
+ // 获取传递的崩溃日志
+ mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO);
+ // 设置系统默认主题(避免自定义主题冲突)
+ setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
- boolean isAppCrashSafetyWireOK() {
- LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
- currentSafetyLevel = loadCurrentSafetyLevel();
- return isSafetyWireWorking(currentSafetyLevel);
- }
+ // 动态创建布局(避免 XML 布局加载异常)
+ setContentView: {
+ // 垂直滚动视图(处理日志过长)
+ ScrollView contentView = new ScrollView(this);
+ contentView.setFillViewport(true);
- // 调用函数以启用持续崩溃保险,从而调用 CrashHandler 内部崩溃处理窗口
- void postResumeCrashSafetyWireHandler(final Context context) {
- new Handler(Looper.getMainLooper()).postDelayed(new Runnable(){
- @Override
- public void run() {
- LogUtils.d(TAG, "Handler run()");
- if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) {
- // 如果下一次应用崩溃时,保险丝熔断,则先恢复保险丝满能状态
- // 进程持续运行时,恢复保险丝熔断值
- //Resume to maximum
- AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
- LogUtils.d(TAG, "postResumeCrashSafetyWireHandler");
- }
- }
- }, 500);
- }
- }
+ // 水平滚动视图(处理日志行过长)
+ HorizontalScrollView hw = new HorizontalScrollView(this);
+ hw.setBackgroundColor(Color.GRAY); // 背景色设为灰色
- public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
- private static final int MENUITEM_COPY = 0;
- private static final int MENUITEM_RESTART = 1;
+ // 日志显示文本框
+ TextView message = new TextView(this);
+ {
+ int padding = dp2px(16); // 内边距 16dp(适配不同屏幕)
+ message.setPadding(padding, padding, padding, padding);
+ message.setText(mLog); // 设置崩溃日志
+ message.setTextColor(Color.BLACK); // 文字黑色
+ message.setTextIsSelectable(true); // 支持文本选择(便于手动复制)
+ }
- private String mLog;
+ // 组装布局:TextView -> HorizontalScrollView -> ScrollView
+ hw.addView(message);
+ contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ // 设置当前 Activity 布局
+ setContentView(contentView);
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
+ // 配置 ActionBar 标题和副标题
+ getActionBar().setTitle(TITTLE);
+ getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
+ }
+ }
- mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO);
- setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
- setContentView: {
- ScrollView contentView = new ScrollView(this);
- contentView.setFillViewport(true);
+ /**
+ * 重写返回键逻辑:点击返回键直接重启应用
+ */
+ @Override
+ public void onBackPressed() {
+ restart();
+ }
- HorizontalScrollView hw = new HorizontalScrollView(this);
- hw.setBackgroundColor(Color.GRAY);
- TextView message = new TextView(this); {
- int padding = dp2px(16);
- message.setPadding(padding, padding, padding, padding);
- message.setText(mLog);
- message.setTextColor(Color.BLACK);
- message.setTextIsSelectable(true);
- }
- hw.addView(message);
+ /**
+ * 重启当前应用(与 GlobalCrashActivity 逻辑一致)
+ * 清除任务栈,启动主 Activity,终止当前进程
+ */
+ private void restart() {
+ PackageManager pm = getPackageManager();
+ // 获取应用启动意图(默认启动主 Activity)
+ Intent intent = pm.getLaunchIntentForPackage(getPackageName());
+ if (intent != null) {
+ // 设置意图标志:清除原有任务栈,创建新任务
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ );
+ startActivity(intent);
+ }
+ // 关闭当前页面,终止进程,确保完全重启
+ finish();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+ }
- contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- setContentView(contentView);
- getActionBar().setTitle(TITTLE);
- getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
- }
- }
+ /**
+ * dp 转 px(适配不同屏幕密度)
+ * @param dpValue dp 值
+ * @return 转换后的 px 值
+ */
+ private int dp2px(final float dpValue) {
+ final float scale = Resources.getSystem().getDisplayMetrics().density;
+ return (int) (dpValue * scale + 0.5f); // 四舍五入确保精度
+ }
- @Override
- public void onBackPressed() {
- restart();
- }
+ /**
+ * 菜单点击事件回调(处理复制、重启)
+ * @param item 被点击的菜单项
+ * @return false:不消费事件(保持默认行为)
+ */
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENUITEM_COPY:
+ // 复制日志到剪贴板
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
+ Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
+ break;
+ case MENUITEM_RESTART:
+ // 恢复保险丝到最高等级,然后重启应用
+ AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
+ restart();
+ break;
+ }
+ return false;
+ }
- private void restart() {
- PackageManager pm = getPackageManager();
- Intent intent = pm.getLaunchIntentForPackage(getPackageName());
- if (intent != null) {
- intent.addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK
- );
- startActivity(intent);
- }
- finish();
- android.os.Process.killProcess(android.os.Process.myPid());
- System.exit(0);
- }
-
- private int dp2px(final float dpValue) {
- final float scale = Resources.getSystem().getDisplayMetrics().density;
- return (int) (dpValue * scale + 0.5f);
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case MENUITEM_COPY:
- ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
- Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
- break;
- case MENUITEM_RESTART:
- AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
- restart();
- break;
- }
- return false;
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, MENUITEM_COPY, 0, "Copy").setOnMenuItemClickListener(this)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- menu.add(0, MENUITEM_RESTART, 0, "Restart").setOnMenuItemClickListener(this)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- return true;
- }
- }
+ /**
+ * 创建 ActionBar 菜单(添加复制、重启项)
+ * @param menu 菜单容器
+ * @return true:显示菜单
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // 添加「复制」菜单:有空间时显示在 ActionBar,否则放入溢出菜单
+ menu.add(0, MENUITEM_COPY, 0, "Copy")
+ .setOnMenuItemClickListener(this)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ // 添加「重启」菜单:同上
+ menu.add(0, MENUITEM_RESTART, 0, "Restart")
+ .setOnMenuItemClickListener(this)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+ }
}
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java
index 2500de20..2c734afb 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java
@@ -1,70 +1,174 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/01/05 10:10:23
- * @Describe 全局应用类
- */
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import cc.winboll.studio.libappbase.GlobalApplication;
+import android.content.pm.PackageManager.NameNotFoundException;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 19:56
+ * @Describe 全局 Application 类,用于初始化应用核心组件、管理全局状态(如调试模式)
+ * 需在 AndroidManifest.xml 中配置 android:name=".GlobalApplication" 使其生效
+ */
public class GlobalApplication extends Application {
+ /** 日志标签 */
public static final String TAG = "GlobalApplication";
-
- // 应用是否处于调试状态
- volatile static boolean isDebuging = false;
-
- public static void setIsDebuging(boolean isDebuging) {
- GlobalApplication.isDebuging = isDebuging;
+
+ /** 全局 Application 单例实例(volatile 保证多线程可见性,避免指令重排) */
+ private static volatile GlobalApplication sInstance;
+
+ /**
+ * 应用调试模式标记(volatile 保证多线程可见性)
+ * true:调试模式(开启日志、调试功能);false:正式模式(关闭调试相关功能)
+ */
+ private static volatile boolean isDebugging = false;
+
+ /**
+ * 获取全局 Application 单例实例(外部可通过此方法获取上下文)
+ * @return GlobalApplication 单例(未初始化时返回 null,需确保配置 AndroidManifest)
+ */
+ public static GlobalApplication getInstance() {
+ return sInstance;
}
-
+
+ /**
+ * 设置应用调试模式
+ * @param debugging 调试模式状态(true/false)
+ */
+ public static void setIsDebugging(boolean debugging) {
+ isDebugging = debugging;
+ }
+
+ /**
+ * 保存调试模式状态到本地文件(持久化存储,重启应用后生效)
+ * @param application 全局 Application 实例(通过 getInstance() 获取更规范)
+ */
public static void saveDebugStatus(GlobalApplication application) {
- APPModel.saveBeanToFile(application.getAPPModelFilePath(application), new APPModel(GlobalApplication.isDebuging));
+ if (application == null) {
+ LogUtils.e(TAG, "saveDebugStatus: Application 实例为空,保存失败");
+ return;
+ }
+ // 将调试状态封装为 APPModel 并保存到文件
+ APPModel.saveBeanToFile(
+ getAppModelFilePath(application),
+ new APPModel(isDebugging)
+ );
}
-
- static String getAPPModelFilePath(GlobalApplication application) {
+
+ /**
+ * 获取 APPModel 配置文件的存储路径
+ * 路径:应用私有数据目录 / APPModel.json(仅当前应用可访问,安全)
+ * @param application 全局 Application 实例
+ * @return 配置文件绝对路径
+ */
+ private static String getAppModelFilePath(GlobalApplication application) {
return application.getDataDir().getPath() + "/APPModel.json";
}
- public static boolean isDebuging() {
- return isDebuging;
+ /**
+ * 获取当前应用调试模式状态
+ * @return true:调试模式;false:正式模式
+ */
+ public static boolean isDebugging() {
+ return isDebugging;
}
+ /**
+ * 应用启动时初始化(仅执行一次)
+ * 初始化核心框架、恢复调试状态、配置全局异常处理等
+ */
@Override
public void onCreate() {
super.onCreate();
-
- setIsDebuging(true);
- // 添加日志模块
- LogUtils.init(this);
- // 设置应用异常处理窗口
- CrashHandler.init(this);
- // 初始化 Toast 框架
- ToastUtils.init(this);
+ // 初始化单例实例(确保在所有初始化操作前完成)
+ sInstance = this;
+
+ // 初始化基础组件(日志、崩溃处理、Toast)
+ initCoreComponents();
+ // 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
+ restoreDebugStatus();
+
+ LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
+ }
+
+ /**
+ * 初始化应用核心组件(日志、崩溃处理、Toast 框架)
+ */
+ private void initCoreComponents() {
+ // 初始化日志工具(传入 Application 上下文)
+ LogUtils.init(this);
+ // 初始化全局异常处理器(捕获应用崩溃信息,用于调试或上报)
+ CrashHandler.init(this);
+ // 初始化 Toast 工具(统一 Toast 样式、避免内存泄漏等)
+ ToastUtils.init(this);
+ }
+
+ /**
+ * 恢复调试模式状态(从本地配置文件读取)
+ * 1. 读取本地 APPModel.json 文件
+ * 2. 读取成功:使用保存的调试状态;读取失败(文件不存在):默认关闭调试并创建配置文件
+ */
+ private void restoreDebugStatus() {
+ // 从文件加载 APPModel 实例(存储调试状态的模型类)
+ APPModel appModel = APPModel.loadBeanFromFile(
+ getAppModelFilePath(this),
+ APPModel.class
+ );
- // 应用保存的调试标志
- APPModel appModel = APPModel.loadBeanFromFile(getAPPModelFilePath(this), APPModel.class);
if (appModel == null) {
- setIsDebuging(false);
+ // 配置文件不存在,默认关闭调试模式并创建文件
+ setIsDebugging(false);
saveDebugStatus(this);
+ LogUtils.d(TAG, "调试配置文件不存在,默认关闭调试模式并创建配置文件");
} else {
- setIsDebuging(appModel.isDebuging());
+ // 配置文件存在,使用保存的调试状态
+ setIsDebugging(appModel.isDebugging());
+ LogUtils.d(TAG, "从配置文件恢复调试模式:" + isDebugging);
}
}
-
- public static String getAppName(Context context) {
+
+ /**
+ * 获取应用名称(从 AndroidManifest.xml 的 android:label 读取)
+ * @param context 上下文(建议传入 Application 上下文,避免内存泄漏)
+ * @return 应用名称(读取失败返回 null)
+ */
+ public static String getAppName(Context context) {
+ if (context == null) {
+ LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
+ return null;
+ }
PackageManager packageManager = context.getPackageManager();
try {
+ // 获取应用信息(包含应用名称、图标等)
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(
- context.getPackageName(), 0);
- return (String) packageManager.getApplicationLabel(applicationInfo);
- } catch (PackageManager.NameNotFoundException e) {
+ context.getPackageName(), // 当前应用包名
+ 0 // 额外标志(0 表示默认获取基本信息)
+ );
+ // 从应用信息中获取应用名称(支持多语言)
+ String appName = (String) packageManager.getApplicationLabel(applicationInfo);
+ LogUtils.d(TAG, "获取应用名称成功:" + appName);
+ return appName;
+ } catch (NameNotFoundException e) {
+ // 包名不存在(理论上不会发生,捕获异常避免崩溃)
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ //LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
return null;
}
+
+ /**
+ * 应用终止时调用(仅用于释放全局资源)
+ */
+ @Override
+ public void onTerminate() {
+ super.onTerminate();
+ // 释放单例引用(可选,避免内存泄漏风险)
+ sInstance = null;
+ LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java
index b4ecda5a..1d0519d6 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java
@@ -1,9 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/02/11 00:14:05
- */
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
@@ -16,79 +12,175 @@ import android.view.MenuItem;
import android.widget.Toast;
import cc.winboll.studio.libappbase.R;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 19:58
+ * @Describe 应用异常报告观察活动窗口类
+ * 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
+ * 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用
+ */
public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
- private static final int MENUITEM_COPY = 0;
- private static final int MENUITEM_RESTART = 1;
-
- GlobalCrashReportView mGlobalCrashReportView;
- String mLog;
-
-
+ /** 日志标签(用于调试日志输出,唯一标识当前 Activity) */
public static final String TAG = "GlobalCrashActivity";
+ /** 菜单标识:复制崩溃日志(用于区分菜单项点击事件) */
+ private static final int MENU_ITEM_COPY = 0;
+ /** 菜单标识:重启应用(用于区分菜单项点击事件) */
+ private static final int MENU_ITEM_RESTART = 1;
+
+ /** 崩溃报告展示自定义视图 */
+ // 负责渲染崩溃日志文本、提供 Toolbar 容器,封装了日志展示和菜单样式控制逻辑
+ private GlobalCrashReportView mCrashReportView;
+
+ /** 崩溃日志文本内容 */
+ // 从 CrashHandler 通过 Intent 传递过来,包含异常堆栈、设备信息等完整崩溃数据
+ private String mCrashLog;
+
+ /**
+ * Activity 创建时初始化(生命周期核心方法,仅执行一次)
+ * @param savedInstanceState 保存的实例状态(崩溃页面无需恢复状态,此处仅作兼容)
+ */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- CrashHandler.AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
- mLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
- //setTheme(android.R.style.Theme_Holo_Light_NoActionBar);
- //setTheme(R.style.APPBaseTheme);
+ // 初始化崩溃安全防护机制
+ // 作用:防止应用重启后短时间内再次崩溃,由 CrashHandler 内部实现防护逻辑
+ CrashHandler.AppCrashSafetyWire.getInstance()
+ .postResumeCrashSafetyWireHandler(getApplicationContext());
+
+ // 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
+ mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
+
+ // 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
setContentView(R.layout.activity_globalcrash);
- mGlobalCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
- mGlobalCrashReportView.setReport(mLog);
- setActionBar(mGlobalCrashReportView.getToolbar());
-
- getActionBar().setTitle(CrashHandler.TITTLE);
- getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()));
+
+ // 初始化崩溃报告展示视图(通过布局 ID 找到自定义 View 实例)
+ mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
+ // 将崩溃日志设置到视图中,由自定义 View 负责排版和显示
+ mCrashReportView.setReport(mCrashLog);
+
+ // 设置页面的 ActionBar(复用自定义 View 中的 Toolbar 作为系统 ActionBar)
+ setActionBar(mCrashReportView.getToolbar());
+
+ // 配置 ActionBar 标题和副标题(非空判断避免空指针异常)
+ if (getActionBar() != null) {
+ // 设置标题:使用 CrashHandler 中定义的统一标题(如 "应用崩溃报告")
+ getActionBar().setTitle(CrashHandler.TITTLE);
+ // 设置副标题:显示当前应用名称(从全局 Application 工具方法获取)
+ getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()));
+ }
}
+ /**
+ * 重写返回键点击事件
+ * 逻辑:点击手机返回键时,直接重启应用(而非返回上一页,因崩溃后上一页状态可能异常)
+ */
@Override
public void onBackPressed() {
- restart();
+ restartApp();
}
- private void restart() {
- PackageManager pm = getPackageManager();
- Intent intent = pm.getLaunchIntentForPackage(getPackageName());
- if (intent != null) {
- intent.addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK
- );
- startActivity(intent);
+ /**
+ * 重启当前应用(核心工具方法)
+ * 实现逻辑:
+ * 1. 获取应用的启动意图(默认启动 AndroidManifest 中配置的主 Activity)
+ * 2. 设置意图标志,清除原有任务栈,避免残留异常页面
+ * 3. 启动主 Activity 并终止当前进程,确保应用完全重启
+ */
+ private void restartApp() {
+ // 获取 PackageManager 实例(用于获取应用相关信息和意图)
+ PackageManager packageManager = getPackageManager();
+ // 获取应用的启动意图(参数为当前应用包名,返回主 Activity 的意图)
+ Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName());
+
+ if (launchIntent != null) {
+ // 设置意图标志:
+ // FLAG_ACTIVITY_NEW_TASK:创建新的任务栈启动 Activity
+ // FLAG_ACTIVITY_CLEAR_TOP:清除目标 Activity 之上的所有 Activity
+ // FLAG_ACTIVITY_CLEAR_TASK:清除当前任务栈中的所有 Activity
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ // 启动应用主 Activity
+ startActivity(launchIntent);
}
+
+ // 关闭当前崩溃报告页面
finish();
+ // 终止当前应用进程(确保释放所有资源,避免内存泄漏)
android.os.Process.killProcess(android.os.Process.myPid());
+ // 强制退出虚拟机(彻底终止应用,防止残留线程继续运行)
System.exit(0);
}
+ /**
+ * 菜单项点击事件回调(实现 MenuItem.OnMenuItemClickListener 接口)
+ * @param item 被点击的菜单项实例
+ * @return boolean:true 表示事件已消费,不再向下传递;false 表示未消费
+ */
@Override
public boolean onMenuItemClick(MenuItem item) {
+ // 根据菜单项 ID 判断点击的是哪个功能
switch (item.getItemId()) {
- case MENUITEM_COPY:
- ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
- Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
+ case MENU_ITEM_COPY:
+ // 点击「复制」菜单,执行复制崩溃日志到剪贴板
+ copyCrashLogToClipboard();
break;
- case MENUITEM_RESTART:
+ case MENU_ITEM_RESTART:
+ // 点击「重启」菜单:先恢复崩溃防护机制到最大等级,再重启应用
CrashHandler.AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
- restart();
+ restartApp();
break;
}
return false;
}
+ /**
+ * 创建页面顶部菜单(ActionBar 菜单)
+ * @param menu 菜单容器,用于添加菜单项
+ * @return boolean:true 表示显示菜单;false 表示不显示
+ */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, MENUITEM_COPY, 0, "Copy").setOnMenuItemClickListener(this)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- menu.add(0, MENUITEM_RESTART, 0, "Restart").setOnMenuItemClickListener(this)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-
- // 更新菜单文字风格
- mGlobalCrashReportView.updateMenuStyle();
+ // 添加「复制」菜单项:
+ // 参数说明:菜单组 ID(0 表示默认组)、菜单项 ID(MENU_ITEM_COPY)、排序号(0)、菜单文本("Copy")
+ // setOnMenuItemClickListener(this):绑定点击事件到当前 Activity
+ // setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM):有空间时显示在 ActionBar 上,否则放入溢出菜单
+ menu.add(0, MENU_ITEM_COPY, 0, "Copy")
+ .setOnMenuItemClickListener(this)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ // 添加「重启」菜单项(参数含义同上)
+ menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
+ .setOnMenuItemClickListener(this)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ // 调用自定义视图的方法,更新菜单文字样式(如颜色、字体大小等,由自定义 View 内部实现)
+ mCrashReportView.updateMenuStyle();
+
return true;
}
+
+ /**
+ * 将崩溃日志复制到系统剪贴板(工具方法)
+ * 功能:用户点击复制菜单后,将完整崩溃日志存入剪贴板,方便粘贴到聊天工具或文档中
+ */
+ private void copyCrashLogToClipboard() {
+ // 获取系统剪贴板服务(需通过 getSystemService 方法获取)
+ ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+
+ // 创建剪贴板数据:
+ // 参数 1:标签(用于标识剪贴板内容来源,此处用应用包名)
+ // 参数 2:实际复制的文本内容(崩溃日志)
+ ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog);
+
+ // 将数据设置到剪贴板(完成复制操作)
+ clipboardManager.setPrimaryClip(clipData);
+
+ // 显示复制成功的 Toast 提示(告知用户操作结果)
+ Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java
index fe68a7be..91518229 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/02/11 20:18:30
- * @Describe 应用崩溃报告视图
- */
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -18,121 +13,297 @@ import android.widget.TextView;
import android.widget.Toolbar;
import cc.winboll.studio.libappbase.R;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:21
+ * @Describe 全局崩溃报告视图控件
+ * 用于展示应用崩溃信息,包含顶部工具栏和崩溃日志文本区域,支持自定义配色
+ */
public class GlobalCrashReportView extends LinearLayout {
- public static final String TAG = "GlobalCrashReportView";
+ // 日志标签
+ public static final String TAG = "GlobalCrashReportView";
- Context mContext;
- Toolbar mToolbar;
- int colorTittle;
- int colorTittleBackground;
- int colorText;
- int colorTextBackground;
- TextView mtvReport;
+ // 上下文对象
+ private Context mContext;
+ // 顶部工具栏(标题栏)
+ private Toolbar mToolbar;
+ // 标题文字颜色
+ private int mTitleColor;
+ // 标题栏背景颜色
+ private int mTitleBackgroundColor;
+ // 日志文本颜色
+ private int mTextColor;
+ // 日志区域背景颜色
+ private int mTextBackgroundColor;
+ // 崩溃日志显示文本控件
+ private TextView mTvReport;
- public GlobalCrashReportView(Context context) {
- super(context);
- mContext = context;
- //initView();
- }
+ /**
+ * 构造方法:仅上下文
+ * @param context 上下文
+ */
+ public GlobalCrashReportView(Context context) {
+ super(context);
+ mContext = context;
+ // 初始化默认配置(无自定义属性)
+ initDefaultConfig();
+ }
- public GlobalCrashReportView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- initView(attrs);
- }
+ /**
+ * 构造方法:上下文 + 自定义属性
+ * @param context 上下文
+ * @param attrs 自定义属性集合
+ */
+ public GlobalCrashReportView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ // 初始化视图(解析自定义属性)
+ initView(attrs);
+ }
- public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContext = context;
- //initView();
- }
+ /**
+ * 构造方法:上下文 + 自定义属性 + 样式属性
+ * @param context 上下文
+ * @param attrs 自定义属性集合
+ * @param defStyleAttr 样式属性
+ */
+ public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mContext = context;
+ // 初始化视图(解析自定义属性)
+ initView(attrs);
+ }
- public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
- //initView();
- }
+ /**
+ * 构造方法:上下文 + 自定义属性 + 样式属性 + 样式资源
+ * @param context 上下文
+ * @param attrs 自定义属性集合
+ * @param defStyleAttr 样式属性
+ * @param defStyleRes 样式资源
+ */
+ public GlobalCrashReportView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mContext = context;
+ // 初始化视图(解析自定义属性)
+ initView(attrs);
+ }
- public void setColorTittle(int colorTittle) {
- this.colorTittle = colorTittle;
- }
+ /**
+ * 设置标题文字颜色
+ * @param titleColor 颜色值(如 Color.WHITE 或 #FFFFFF)
+ */
+ public void setTitleColor(int titleColor) {
+ this.mTitleColor = titleColor;
+ // 实时更新工具栏标题颜色
+ if (mToolbar != null) {
+ mToolbar.setTitleTextColor(titleColor);
+ mToolbar.setSubtitleTextColor(titleColor);
+ }
+ }
- public int getColorTittle() {
- return colorTittle;
- }
+ /**
+ * 获取标题文字颜色
+ * @return 标题文字颜色值
+ */
+ public int getTitleColor() {
+ return mTitleColor;
+ }
- public void setColorTittleBackground(int colorTittleBackground) {
- this.colorTittleBackground = colorTittleBackground;
- }
+ /**
+ * 设置标题栏背景颜色
+ * @param titleBackgroundColor 颜色值(如 Color.BLACK 或 #000000)
+ */
+ public void setTitleBackgroundColor(int titleBackgroundColor) {
+ this.mTitleBackgroundColor = titleBackgroundColor;
+ // 实时更新工具栏背景颜色
+ if (mToolbar != null) {
+ mToolbar.setBackgroundColor(titleBackgroundColor);
+ }
+ }
- public int getColorTittleBackground() {
- return colorTittleBackground;
- }
+ /**
+ * 获取标题栏背景颜色
+ * @return 标题栏背景颜色值
+ */
+ public int getTitleBackgroundColor() {
+ return mTitleBackgroundColor;
+ }
- public void setColorText(int colorText) {
- this.colorText = colorText;
- }
+ /**
+ * 设置日志文本颜色
+ * @param textColor 颜色值(如 Color.BLACK 或 #000000)
+ */
+ public void setTextColor(int textColor) {
+ this.mTextColor = textColor;
+ // 实时更新日志文本颜色
+ if (mTvReport != null) {
+ mTvReport.setTextColor(textColor);
+ }
+ }
- public int getColorText() {
- return colorText;
- }
+ /**
+ * 获取日志文本颜色
+ * @return 日志文本颜色值
+ */
+ public int getTextColor() {
+ return mTextColor;
+ }
- public void setColorTextBackground(int colorTextBackground) {
- this.colorTextBackground = colorTextBackground;
- }
+ /**
+ * 设置日志区域背景颜色
+ * @param textBackgroundColor 颜色值(如 Color.WHITE 或 #FFFFFF)
+ */
+ public void setTextBackgroundColor(int textBackgroundColor) {
+ this.mTextBackgroundColor = textBackgroundColor;
+ // 实时更新日志区域和主布局背景颜色
+ if (mTvReport != null) {
+ mTvReport.setBackgroundColor(textBackgroundColor);
+ }
+ setBackgroundColor(textBackgroundColor);
+ }
- public int getColorTextBackground() {
- return colorTextBackground;
- }
+ /**
+ * 获取日志区域背景颜色
+ * @return 日志区域背景颜色值
+ */
+ public int getTextBackgroundColor() {
+ return mTextBackgroundColor;
+ }
- void initView(AttributeSet attrs) {
- TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.GlobalCrashActivity, R.attr.themeGlobalCrashActivity, 0);
- this.colorTittle = a.getColor(R.styleable.GlobalCrashActivity_colorTittle, Color.WHITE);
- this.colorTittleBackground = a.getColor(R.styleable.GlobalCrashActivity_colorTittleBackgound, Color.BLACK);
- this.colorText = a.getColor(R.styleable.GlobalCrashActivity_colorText, Color.BLACK);
- this.colorTextBackground = a.getColor(R.styleable.GlobalCrashActivity_colorTextBackgound, Color.WHITE);
- // 返回一个绑定资源结束的信号给资源
- a.recycle();
+ /**
+ * 初始化默认配置(无自定义属性时使用)
+ */
+ private void initDefaultConfig() {
+ // 设置默认配色
+ mTitleColor = Color.WHITE;
+ mTitleBackgroundColor = Color.BLACK;
+ mTextColor = Color.BLACK;
+ mTextBackgroundColor = Color.WHITE;
+ // 加载布局
+ inflateView();
+ // 初始化控件样式
+ initWidgetStyle();
+ }
- /*this.colorTittle = Color.WHITE;
- this.colorTittleBackground = Color.BLACK;
- this.colorText = Color.BLACK;
- this.colorTextBackground = Color.WHITE;
- */
-
- inflate(mContext, R.layout.view_globalcrashreport, this);
+ /**
+ * 初始化视图(解析自定义属性 + 加载布局 + 设置样式)
+ * @param attrs 自定义属性集合
+ */
+ private void initView(AttributeSet attrs) {
+ // 解析自定义属性(关联 attrs.xml 中的 GlobalCrashActivity 样式)
+ TypedArray typedArray = mContext.obtainStyledAttributes(
+ attrs,
+ R.styleable.GlobalCrashActivity,
+ R.attr.themeGlobalCrashActivity,
+ 0
+ );
- LinearLayout llMain = findViewById(R.id.viewglobalcrashreportLinearLayout1);
- llMain.setBackgroundColor(this.colorTextBackground);
- mToolbar = findViewById(R.id.viewglobalcrashreportToolbar1);
- mToolbar.setBackgroundColor(this.colorTittleBackground);
- mToolbar.setTitleTextColor(this.colorTittle);
- mToolbar.setSubtitleTextColor(this.colorTittle);
- mtvReport = findViewById(R.id.viewglobalcrashreportTextView1);
- mtvReport.setTextColor(this.colorText);
- mtvReport.setBackgroundColor(this.colorTextBackground);
- }
+ // 读取自定义属性值(无设置时使用默认值)
+ mTitleColor = typedArray.getColor(
+ R.styleable.GlobalCrashActivity_colorTittle,
+ Color.WHITE
+ );
+ mTitleBackgroundColor = typedArray.getColor(
+ R.styleable.GlobalCrashActivity_colorTittleBackgound, // 注:原拼写错误(Backgound→Background),保持与 attrs.xml 一致
+ Color.BLACK
+ );
+ mTextColor = typedArray.getColor(
+ R.styleable.GlobalCrashActivity_colorText,
+ Color.BLACK
+ );
+ mTextBackgroundColor = typedArray.getColor(
+ R.styleable.GlobalCrashActivity_colorTextBackgound, // 注:原拼写错误,保持与 attrs.xml 一致
+ Color.WHITE
+ );
- public void setReport(String report) {
- mtvReport.setText(report);
- }
+ // 回收 TypedArray,避免内存泄漏
+ typedArray.recycle();
- public Toolbar getToolbar() {
- return mToolbar;
- }
+ // 加载布局文件
+ inflateView();
+ // 初始化控件样式
+ initWidgetStyle();
+ }
- //
- // 更新菜单文字风格
- //
- public void updateMenuStyle() {
- // 设置菜单文本颜色
- Menu menu = mToolbar.getMenu();
- for (int i = 0; i < menu.size(); i++) {
- MenuItem item = menu.getItem(i);
- SpannableString spanString = new SpannableString(item.getTitle().toString());
- spanString.setSpan(new ForegroundColorSpan(this.colorTittle), 0, spanString.length(), 0);
- item.setTitle(spanString);
- }
- }
+ /**
+ * 加载布局文件
+ */
+ private void inflateView() {
+ // 加载自定义布局(R.layout.view_globalcrashreport)
+ inflate(mContext, R.layout.view_globalcrashreport, this);
+ // 绑定控件
+ mToolbar = findViewById(R.id.viewglobalcrashreportToolbar1);
+ mTvReport = findViewById(R.id.viewglobalcrashreportTextView1);
+ }
+
+ /**
+ * 初始化控件样式(设置配色和基础属性)
+ */
+ private void initWidgetStyle() {
+ // 设置主布局背景颜色
+ setBackgroundColor(mTextBackgroundColor);
+
+ // 配置工具栏样式
+ if (mToolbar != null) {
+ mToolbar.setBackgroundColor(mTitleBackgroundColor);
+ mToolbar.setTitleTextColor(mTitleColor);
+ mToolbar.setSubtitleTextColor(mTitleColor);
+ }
+
+ // 配置日志文本控件样式
+ if (mTvReport != null) {
+ mTvReport.setTextColor(mTextColor);
+ mTvReport.setBackgroundColor(mTextBackgroundColor);
+ // 可选:设置日志文本换行方式(默认已换行,此处增强可读性)
+ mTvReport.setSingleLine(false);
+ mTvReport.setHorizontallyScrolling(false);
+ }
+ }
+
+ /**
+ * 设置崩溃报告内容到文本控件
+ * @param report 崩溃日志字符串(通常包含异常信息、调用栈等)
+ */
+ public void setReport(String report) {
+ if (mTvReport != null) {
+ mTvReport.setText(report);
+ }
+ }
+
+ /**
+ * 获取顶部工具栏对象(用于外部设置标题、添加菜单等)
+ * @return Toolbar 实例
+ */
+ public Toolbar getToolbar() {
+ return mToolbar;
+ }
+
+ /**
+ * 更新工具栏菜单文字颜色(与标题颜色保持一致)
+ * 需在菜单加载完成后调用(如 Toolbar 加载菜单后)
+ */
+ public void updateMenuStyle() {
+ if (mToolbar == null) return;
+
+ // 获取工具栏菜单
+ Menu menu = mToolbar.getMenu();
+ if (menu == null || menu.size() == 0) return;
+
+ // 遍历所有菜单项,设置文字颜色
+ for (int i = 0; i < menu.size(); i++) {
+ MenuItem menuItem = menu.getItem(i);
+ String title = menuItem.getTitle().toString();
+ // 使用 SpannableString 设置文字颜色
+ SpannableString spanString = new SpannableString(title);
+ spanString.setSpan(
+ new ForegroundColorSpan(mTitleColor),
+ 0,
+ spanString.length(),
+ 0 // Spannable.SPAN_INCLUSIVE_EXCLUSIVE(默认值,包含起始位置,不包含结束位置)
+ );
+ menuItem.setTitle(spanString);
+ }
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java
index bcd5d1f3..1a48fb61 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java
@@ -1,129 +1,240 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @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;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:26
+ * @Describe 水平滚动 ListView 控件
+ * 继承自 ListView,重写布局和测量逻辑,实现子项水平排列和滚动,替代默认垂直布局
+ */
public class HorizontalListView extends ListView {
- public static final String TAG = "HorizontalListView";
- private int verticalOffset = 0;
- private Scroller scroller;
- private int totalWidth;
+ /** 日志标签,用于当前控件的日志输出标识 */
+ public static final String TAG = "HorizontalListView";
- public HorizontalListView(Context context) {
- super(context);
- init();
- }
+ /** 子项垂直偏移量(用于调整子项在垂直方向的位置,默认 0) */
+ private int mVerticalOffset = 0;
+ /** 平滑滚动控制器(用于实现水平方向的平滑滚动动画) */
+ private Scroller mScroller;
+ /** 所有子项总宽度(包含内边距),用于计算滚动范围 */
+ private int mTotalWidth;
- public HorizontalListView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
+ /**
+ * 构造方法:仅上下文
+ * @param context 上下文(Activity/Fragment)
+ */
+ public HorizontalListView(Context context) {
+ super(context);
+ init();
+ }
- public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init();
- }
+ /**
+ * 构造方法:上下文 + 自定义属性
+ * @param context 上下文
+ * @param attrs 自定义属性集合(如布局文件中设置的属性)
+ */
+ public HorizontalListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
- private void init() {
- scroller = new Scroller(getContext());
- setHorizontalScrollBarEnabled(true);
- setVerticalScrollBarEnabled(false);
- }
+ /**
+ * 构造方法:上下文 + 自定义属性 + 样式属性
+ * @param context 上下文
+ * @param attrs 自定义属性集合
+ * @param defStyle 样式属性(如系统默认样式)
+ */
+ public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
- public void setVerticalOffset(int verticalOffset) {
- this.verticalOffset = verticalOffset;
- }
+ /**
+ * 初始化控件配置
+ * 初始化滚动控制器,设置滚动条显示状态
+ */
+ private void init() {
+ // 初始化平滑滚动器(上下文为当前控件所在上下文)
+ mScroller = new Scroller(getContext());
+ // 启用水平滚动条(默认显示)
+ setHorizontalScrollBarEnabled(true);
+ // 禁用垂直滚动条(水平列表无需垂直滚动)
+ setVerticalScrollBarEnabled(false);
+ }
- @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;
+ /**
+ * 设置子项垂直偏移量
+ * 用于整体调整所有子项在垂直方向的位置(如居中、偏移)
+ * @param verticalOffset 垂直偏移像素值(正数向下偏移,负数向上偏移)
+ */
+ public void setVerticalOffset(int verticalOffset) {
+ this.mVerticalOffset = verticalOffset;
+ }
- 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();
- }
+ /**
+ * 重写布局方法:实现子项水平排列
+ * 遍历所有子项,按水平方向依次布局(左对齐,叠加排列)
+ * @param changed 布局是否发生变化(true:首次布局或尺寸变化;false:重绘)
+ * @param l 控件左边界坐标
+ * @param t 控件上边界坐标
+ * @param r 控件右边界坐标
+ * @param b 控件下边界坐标
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b); // 执行父类布局逻辑(确保基础配置生效)
- @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);
- }
+ int childCount = getChildCount(); // 获取当前可见子项数量
+ int left = getPaddingLeft(); // 子项起始左坐标(包含控件左内边距)
+ // 控件可用高度(总高度 - 上下内边距)
+ int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+ mTotalWidth = left; // 初始化总宽度为左内边距
- @Override
- public void computeScroll() {
- if (scroller.computeScrollOffset()) {
- scrollTo(scroller.getCurrX(), scroller.getCurrY());
- postInvalidate();
- }
- }
+ // 遍历子项,水平排列
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i); // 获取当前子项
+ int childWidth = child.getMeasuredWidth(); // 子项测量宽度
+ int childHeight = child.getMeasuredHeight(); // 子项测量高度
- public void smoothScrollTo(int x, int y) {
- int dx = x - getScrollX();
- int dy = y - getScrollY();
- scroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); // 300ms平滑动画
- invalidate();
- }
+ // 布局子项:水平方向从 left 开始,垂直方向偏移 mVerticalOffset
+ child.layout(
+ left, // 子项左边界
+ mVerticalOffset, // 子项上边界(带垂直偏移)
+ left + childWidth, // 子项右边界(左 + 宽度)
+ mVerticalOffset + childHeight // 子项下边界(偏移 + 高度)
+ );
- @Override
- public int computeHorizontalScrollRange() {
- return totalWidth;
- }
+ left += childWidth; // 更新下一个子项的起始左坐标
+ }
- @Override
- public int computeHorizontalScrollOffset() {
- return getScrollX();
- }
+ // 计算总宽度(所有子项宽度 + 左右内边距)
+ mTotalWidth = left + getPaddingRight();
+ }
- @Override
- public int computeHorizontalScrollExtent() {
- return getWidth();
- }
+ /**
+ * 重写测量方法:设置控件测量规则
+ * 水平方向:允许无限宽度(适应所有子项总宽度);垂直方向:自适应内容高度
+ * @param widthMeasureSpec 父控件传递的宽度测量规格
+ * @param heightMeasureSpec 父控件传递的高度测量规格
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // 重写宽度测量规则:最大值(Integer.MAX_VALUE >> 2 避免溢出),自适应内容
+ int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ // 重写高度测量规则:同上,自适应子项高度
+ int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
- public void scrollToItem(int position) {
- if (position < 0 || position >= getChildCount()) {
- LogUtils.d(TAG, "无效的position: " + position);
- return;
- }
+ // 执行父类测量逻辑(使用重写后的测量规格)
+ super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
+ }
- View targetView = getChildAt(position);
- int targetLeft = targetView.getLeft();
- int scrollX = targetLeft - getPaddingLeft();
+ /**
+ * 重写滚动计算方法:实现平滑滚动
+ * 配合 Scroller 实现水平方向的平滑滚动动画(需在滚动时调用 invalidate() 触发)
+ */
+ @Override
+ public void computeScroll() {
+ // 判断滚动是否正在进行(Scroller 计算当前滚动位置)
+ if (mScroller.computeScrollOffset()) {
+ // 滚动到当前计算的位置(x 轴水平滚动,y 轴固定 0)
+ scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
+ // 触发重绘,持续更新滚动状态
+ postInvalidate();
+ }
+ }
- // 修正最大滚动范围计算
- int maxScrollX = totalWidth;
- scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
+ /**
+ * 平滑滚动到指定坐标
+ * 基于 Scroller 实现水平方向的平滑滚动(300ms 动画时长)
+ * @param x 目标 x 轴坐标(水平滚动位置)
+ * @param y 目标 y 轴坐标(固定 0,无需垂直滚动)
+ */
+ public void smoothScrollTo(int x, int y) {
+ // 计算滚动距离(目标坐标 - 当前滚动坐标)
+ int dx = x - getScrollX();
+ int dy = y - getScrollY();
- // 强制重新布局和绘制
- 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);
- }
+ // 启动平滑滚动:起始坐标(当前滚动位置)、滚动距离、动画时长(300ms)
+ mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300);
+ // 触发重绘,启动滚动动画
+ invalidate();
+ }
+
+ /**
+ * 计算水平滚动总范围(用于滚动条显示比例)
+ * @return 滚动总宽度(所有子项总宽度 + 内边距)
+ */
+ @Override
+ public int computeHorizontalScrollRange() {
+ return mTotalWidth;
+ }
+
+ /**
+ * 计算当前水平滚动偏移量(用于滚动条位置)
+ * @return 当前 x 轴滚动坐标
+ */
+ @Override
+ public int computeHorizontalScrollOffset() {
+ return getScrollX();
+ }
+
+ /**
+ * 计算水平滚动可视范围(用于滚动条大小)
+ * @return 控件可见宽度(当前显示区域宽度)
+ */
+ @Override
+ public int computeHorizontalScrollExtent() {
+ return getWidth();
+ }
+
+ /**
+ * 滚动到指定位置的子项(水平方向)
+ * 定位目标子项,计算滚动坐标,执行平滑滚动
+ * @param position 子项索引(从 0 开始,仅当前可见子项有效)
+ */
+ public void scrollToItem(int position) {
+ // 校验索引有效性(避免数组越界)
+ if (position < 0 || position >= getChildCount()) {
+ LogUtils.d(TAG, "无效的子项索引: " + position);
+ return;
+ }
+
+ View targetView = getChildAt(position); // 获取目标子项
+ int targetLeft = targetView.getLeft(); // 目标子项左边界坐标
+ // 计算目标滚动坐标(子项左边界 - 控件左内边距,确保子项左对齐显示)
+ int scrollX = targetLeft - getPaddingLeft();
+
+ // 修正滚动范围(避免超出总宽度或小于 0)
+ int maxScrollX = mTotalWidth - getWidth(); // 最大滚动坐标(总宽度 - 控件宽度)
+ scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
+
+ // 强制重新布局和绘制(确保子项位置正确)
+ requestLayout();
+ invalidateViews();
+ // 平滑滚动到目标坐标
+ smoothScrollTo(scrollX, 0);
+
+ // 打印滚动日志(调试用)
+ LogUtils.d(TAG, String.format(
+ "滚动到子项索引: %d, 目标滚动X: %d, 总滚动范围: %d",
+ position, scrollX, computeHorizontalScrollRange()
+ ));
+ }
+
+ /**
+ * 重置滚动到起始位置(最左侧)
+ * 强制重新布局后,平滑滚动到 x=0 坐标
+ */
+ public void resetScrollToStart() {
+ // 强制重新布局和绘制(确保滚动位置准确)
+ requestLayout();
+ invalidateViews();
+ // 平滑滚动到最左侧(x=0,y=0)
+ smoothScrollTo(0, 0);
+ }
}
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java
index 08392428..e182a1be 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/03/25 20:34:47
- * @Describe 应用日志窗口
- */
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -12,35 +7,59 @@ import android.os.Bundle;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.R;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:29
+ * @Describe 应用日志展示 Activity
+ * 用于单独启动窗口展示应用运行日志,依赖 LogView 控件实现日志加载与显示
+ */
public class LogActivity extends Activity {
+ /** 日志标签,用于当前 Activity 的日志输出标识 */
public static final String TAG = "LogActivity";
- LogView mLogView;
+ /** 日志展示控件(用于加载和显示应用日志) */
+ private LogView mLogView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ // 设置布局文件(包含 LogView 控件)
setContentView(R.layout.activity_log);
- //ToastUtils.show("LogActivity onCreate");
+ // 绑定布局中的 LogView 控件
mLogView = findViewById(R.id.logview);
+ // 启动 LogView 日志加载(如实时刷新日志内容)
mLogView.start();
}
@Override
protected void onResume() {
super.onResume();
+ // 恢复 Activity 时重新启动 LogView(确保日志持续更新)
mLogView.start();
}
- public static void startLogActivity(Context context) {
- Intent intent = new Intent(context, LogActivity.class);
- // 打开多任务窗口
+ /**
+ * 启动日志 Activity 的静态方法(外部调用入口)
+ * 配置 Intent 标志,以多任务/分屏模式启动,避免与主应用任务栈冲突
+ * @param context 上下文(Activity/Fragment),用于启动 Activity
+ */
+ public static void startLogActivity(Context context) {
+ // 创建启动当前 Activity 的 Intent
+ Intent intent = new Intent(context, LogActivity.class);
+
+ // 添加 Intent 标志:支持分屏/多窗口模式(API 24+)
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ // 添加 Intent 标志:创建新任务栈(避免并入调用者任务栈)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // 添加 Intent 标志:标记为新文档(多任务窗口中独立显示)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ // 添加 Intent 标志:允许创建多个任务实例(支持多次启动独立窗口)
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ // 启动 Activity
context.startActivity(intent);
- }
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java
index 4525cc28..ca0f29df 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java
@@ -1,13 +1,6 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2024/08/12 13:44:06
- * @Describe LogUtils
- * @Describe 应用日志类
- */
import android.content.Context;
-import cc.winboll.studio.libappbase.GlobalApplication;
import dalvik.system.DexFile;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@@ -28,353 +21,617 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
-
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @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 }
- 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();
+ /** 是否初始化完成标志(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();
- //
- // 初始化函数
- //
+ /**
+ * 初始化日志工具(默认日志级别:Off,不输出日志)
+ * @param context 全局上下文(建议传入 Application 实例)
+ */
public static void init(Context context) {
- _mContext = context;
+ sContext = 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) {
- if (GlobalApplication.isDebuging()) {
- // 初始化日志缓存文件路径
- _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");
+ sContext = context;
+ // 根据 Debug 模式选择存储路径(外部/内部存储)
+ if (GlobalApplication.isDebugging()) {
+ // Debug 模式:存储在外部缓存目录(可通过文件管理器查看)
+ sLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
+ sLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
} else {
- // 初始化日志缓存文件路径
- _mfLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
- if (!_mfLogCacheDir.exists()) {
- _mfLogCacheDir.mkdirs();
+ // Release 模式:存储在内部缓存目录(仅应用自身可访问)
+ sLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
+ sLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
+ }
+
+ // 创建日志文件夹(不存在则创建)
+ 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);
+ }
+
+ // 扫描应用内所有类的 TAG 并添加到过滤映射表
+ scanAllClassTags();
+ // 加载已保存的 TAG 启用状态配置
+ loadTagEnableSettings();
+ // 标记初始化完成
+ sIsInited = true;
+
+ // 打印初始化日志(调试用)
+ d(TAG, String.format("TAG 过滤映射表初始化完成:%s", sTagEnableMap.toString()));
+ }
+
+ /**
+ * 获取 TAG 过滤映射表(外部可通过此方法获取所有 TAG 及其启用状态)
+ * @return TAG 名称与启用状态的映射
+ */
+ public static Map getTagEnableMap() {
+ return sTagEnableMap;
+ }
+
+ /**
+ * 加载已保存的 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);
}
- _mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
-
- // 初始化日志配置文件路径
- _mfLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
- if (!_mfLogDataDir.exists()) {
- _mfLogDataDir.mkdirs();
- }
- _mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
- }
-
-// Toast.makeText(context,
-// "_mfLogUtilsBeanFile : " + _mfLogUtilsBeanFile
-// + "\n_mfLogCatchFile : " + _mfLogCatchFile,
-// Toast.LENGTH_SHORT).show();
-//
- _mLogUtilsBean = LogUtilsBean.loadBeanFromFile(_mfLogUtilsBeanFile.getPath(), LogUtilsBean.class);
- if (_mLogUtilsBean == null) {
- _mLogUtilsBean = new LogUtilsBean();
- _mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
- }
-
- // 加载当前应用下的所有类的 TAG
- addClassTAGList();
- loadTAGBeanSettings();
- _IsInited = true;
- LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
- }
-
- public static Map getMapTAGList() {
- return mapTAGList;
- }
-
- 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());
- }
- }
-
}
}
- static void saveTAGBeanSettings() {
- ArrayList list = new ArrayList();
- for (Map.Entry entry : mapTAGList.entrySet()) {
- list.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
+ /**
+ * 保存当前 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()));
}
- LogUtilsClassTAGBean.saveBeanList(_mContext, list, LogUtilsClassTAGBean.class);
+ // 保存配置列表到文件
+ LogUtilsClassTAGBean.saveBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class);
}
- static void addClassTAGList() {
- //ClassLoader classLoader = getClass().getClassLoader();
+ /**
+ * 扫描应用内所有类的 TAG 并添加到过滤映射表
+ * 1. 通过 DexFile 读取 APK 中所有类;
+ * 2. 过滤指定包名前缀(cc.winboll.studio)的类;
+ * 3. 反射获取类中 public static final String TAG 字段的值;
+ * 4. 将 TAG 加入映射表,默认禁用(false)。
+ */
+ private static void scanAllClassTags() {
try {
- //String packageName = context.getPackageName();
- String packageNamePrefix = "cc.winboll.studio";
- List classNames = new ArrayList<>();
- String apkPath = _mContext.getPackageCodePath();
- //Log.d("APK_PATH", "The APK path is: " + apkPath);
- LogUtils.d(TAG, String.format("apkPath : %s", apkPath));
- //String apkPath = "/data/app/" + packageName + "-";
+ // 应用 APK 路径(通过上下文获取)
+ String apkPath = sContext.getPackageCodePath();
+ d(TAG, String.format("APK 路径:%s", apkPath));
- //DexFile dexfile = new DexFile(apkPath + "1/base.apk");
- DexFile dexfile = new DexFile(apkPath);
+ // 读取 APK 中的所有类
+ DexFile dexFile = new DexFile(apkPath);
+ Enumeration classNames = dexFile.entries();
- int countTemp = 0;
- Enumeration entries = dexfile.entries();
- while (entries.hasMoreElements()) {
- countTemp++;
- String className = entries.nextElement();
- if (className.startsWith(packageNamePrefix)) {
- classNames.add(className);
+ 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);
}
}
- LogUtils.d(TAG, String.format("countTemp : %d\nClassNames size : %d", countTemp, classNames.size()));
+ // 打印扫描统计(调试用)
+ d(TAG, String.format("APK 总类数:%d,目标包下类数:%d", totalClassCount, targetClassNames.size()));
- for (String className : classNames) {
+ // 反射获取每个类的 TAG 字段(Java 7 增强 for 循环)
+ for (String className : targetClassNames) {
try {
Class> clazz = Class.forName(className);
+ // 获取类中所有声明的字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
- if (Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers()) && field.getType() == String.class && "TAG".equals(field.getName())) {
+ // 过滤条件:public static 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);
- //Log.d("TAG_INFO", "Class: " + className + ", TAG value: " + tagValue);
- //LogUtils.d(TAG, String.format("Tag Value : %s", tagValue));
- //mapTAGList.put(tagValue, true);
- mapTAGList.put(tagValue, false);
+ // 添加到映射表,默认禁用
+ sTagEnableMap.put(tagValue, false);
}
}
- } catch (NoClassDefFoundError | ClassNotFoundException | IllegalAccessException e) {
- LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
- //LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
- //Toast.makeText(context, TAG + " : " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ } 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 (IOException e) {
- LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
- //Toast.makeText(context, TAG + " : " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ // 捕获 APK 读取异常
+ d(TAG, e, Thread.currentThread().getStackTrace());
}
}
- public static void setTAGListEnable(String tag, boolean isEnable) {
- Iterator> iterator = mapTAGList.entrySet().iterator();
+ /**
+ * 设置单个 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();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
if (tag.equals(entry.getKey())) {
entry.setValue(isEnable);
- //System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
break;
}
}
- saveTAGBeanSettings();
- LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
+ // 保存配置到文件(持久化)
+ saveTagEnableSettings();
+ d(TAG, String.format("TAG 配置更新:%s", sTagEnableMap.toString()));
}
- public static void setALlTAGListEnable(boolean isEnable) {
- Iterator> iterator = mapTAGList.entrySet().iterator();
+ /**
+ * 设置所有 TAG 的启用状态(批量控制)
+ * @param isEnable 是否启用(true:所有 TAG 均输出日志;false:所有 TAG 均不输出)
+ */
+ public static void setAllTagsEnable(boolean isEnable) {
+ // 遍历映射表,批量更新所有 TAG 的状态(Java 7 迭代器遍历)
+ Iterator> iterator = sTagEnableMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
entry.setValue(isEnable);
- //System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
- saveTAGBeanSettings();
- LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
+ // 保存配置到文件(持久化)
+ saveTagEnableSettings();
+ d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableMap.toString()));
}
+ /**
+ * 设置全局日志级别(控制日志输出的详细程度)
+ * @param logLevel 目标日志级别
+ */
public static void setLogLevel(LOG_LEVEL logLevel) {
- LogUtils._mLogUtilsBean.setLogLevel(logLevel);
- _mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
+ if (sLogConfigBean != null) {
+ sLogConfigBean.setLogLevel(logLevel);
+ // 保存配置到文件(持久化)
+ sLogConfigBean.saveBeanToFile(sLogConfigFile.getPath(), sLogConfigBean);
+ }
}
+ /**
+ * 获取当前全局日志级别
+ * @return 当前日志级别
+ */
public static LOG_LEVEL getLogLevel() {
- return LogUtils._mLogUtilsBean.getLogLevel();
+ return sLogConfigBean != null ? sLogConfigBean.getLogLevel() : LOG_LEVEL.Off;
}
- static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
- if (!_IsInited) {
+ /**
+ * 判断当前日志是否可输出(校验初始化状态、TAG 启用状态、日志级别)
+ * @param tag 日志 TAG
+ * @param logLevel 日志级别
+ * @return true:可输出;false:不可输出
+ */
+ private static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
+ // 未初始化:不输出
+ if (!sIsInited) {
return false;
- }
- if (mapTAGList.get(tag) == null
- || !mapTAGList.get(tag)) {
+ }
+ // TAG 未配置或未启用:不输出
+ if (sTagEnableMap.get(tag) == null || !sTagEnableMap.get(tag)) {
return false;
- }
- if (!isInTheLevel(logLevel)) {
+ }
+ // 日志级别未达到:不输出
+ if (!isLevelMatched(logLevel)) {
return false;
}
return true;
}
- static boolean isInTheLevel(LOG_LEVEL logLevel) {
- return (LogUtils._mLogUtilsBean.getLogLevel().ordinal() == logLevel.ordinal()
- || LogUtils._mLogUtilsBean.getLogLevel().ordinal() > logLevel.ordinal());
+ /**
+ * 判断日志级别是否匹配(当前全局级别 >= 目标级别时可输出)
+ * 例:全局级别为 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();
}
- //
- // 获取应用日志文件夹
- //
+ /**
+ * 获取日志缓存文件夹路径(外部可通过此方法获取日志存储目录)
+ * @return 日志缓存文件夹
+ */
public static File getLogCacheDir() {
- return _mfLogCacheDir;
+ return sLogCacheDir;
}
- //
- // 调试日志写入函数
- //
- 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 日志内容
+ */
+ public static void e(String tag, String message) {
+ if (isLoggable(tag, LOG_LEVEL.Error)) {
+ saveLog(tag, LOG_LEVEL.Error, message);
}
}
- //
- // 调试日志写入函数
- //
- public static void w(String szTAG, String szMessage) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
- saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, 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());
}
}
- //
- // 调试日志写入函数
- //
- public static void i(String szTAG, String szMessage) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
- saveLog(szTAG, LogUtils.LOG_LEVEL.Info, szMessage);
+ /**
+ * 输出 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);
}
}
- //
- // 调试日志写入函数
- //
- public static void d(String szTAG, String szMessage) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
- saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, szMessage);
+ /**
+ * 输出 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());
}
}
- //
- // 调试日志写入函数
- // 包含线程调试堆栈信息
- //
- public static void d(String szTAG, String szMessage, StackTraceElement[] listStackTrace) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
- StringBuilder sbMessage = new StringBuilder(szMessage);
- sbMessage.append(" \nAt ");
- sbMessage.append(listStackTrace[2].getMethodName());
- sbMessage.append(" (");
- sbMessage.append(listStackTrace[2].getFileName());
- sbMessage.append(":");
- sbMessage.append(listStackTrace[2].getLineNumber());
- sbMessage.append(")");
- saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.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);
}
}
- //
- // 调试日志写入函数
- // 包含异常信息和线程调试堆栈信息
- //
- public static void d(String szTAG, Exception e, StackTraceElement[] listStackTrace) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
- StringBuilder sbMessage = new StringBuilder(e.getClass().toGenericString());
- sbMessage.append(" : ");
- sbMessage.append(e.getMessage());
- sbMessage.append(" \nAt ");
- sbMessage.append(listStackTrace[2].getMethodName());
- sbMessage.append(" (");
- sbMessage.append(listStackTrace[2].getFileName());
- sbMessage.append(":");
- sbMessage.append(listStackTrace[2].getLineNumber());
- sbMessage.append(")");
- saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
+ /**
+ * 输出 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);
}
}
- //
- // 调试日志写入函数
- //
- public static void v(String szTAG, String szMessage) {
- if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
- saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, szMessage);
+ /**
+ * 输出 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());
}
}
- //
- // 日志文件保存函数
- //
- static void saveLog(String szTAG, LogUtils.LOG_LEVEL logLevel, String szMessage) {
+ /**
+ * 输出 Debug 级别日志(带异常信息和调用栈)
+ * 包含异常类型、异常信息、调用位置,便于异常定位
+ * @param tag TAG 名称
+ * @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());
+ }
+ }
+
+ /**
+ * 输出 Debug 级别日志(带日志内容+异常对象,简化调用)
+ * 无需手动传入调用栈,内部自动获取,适配常见调试场景
+ * @param tag TAG 名称
+ * @param message 日志内容
+ * @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());
+ }
+ }
+
+ /**
+ * 输出 Verbose 级别日志(最详细级别)
+ * @param tag TAG 名称
+ * @param message 日志内容
+ */
+ public static void v(String tag, String message) {
+ if (isLoggable(tag, LOG_LEVEL.Verbose)) {
+ saveLog(tag, LOG_LEVEL.Verbose, message);
+ }
+ }
+
+ /**
+ * 核心日志保存方法(将日志写入文件)
+ * 日志格式:[级别] [时间戳] [TAG]
+ * 日志内容
+ * @param tag TAG 名称
+ * @param logLevel 日志级别
+ * @param message 日志内容
+ */
+ private static void saveLog(String tag, LOG_LEVEL logLevel, String message) {
+ BufferedWriter writer = null;
try {
- BufferedWriter out = null;
- out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(_mfLogCatchFile, true), "UTF-8"));
- out.write("[" + logLevel + "] " + mSimpleDateFormat.format(System.currentTimeMillis()) + " [" + szTAG + "]\n" + szMessage + "\n");
- out.close();
+ // 以追加模式打开日志文件,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);
} catch (IOException e) {
- LogUtils.d(TAG, "IOException : " + e.getMessage());
- }
- }
-
- //
- // 历史日志加载函数
- //
- public static String loadLog() {
- if (_mfLogCatchFile.exists()) {
- StringBuffer sb = new StringBuffer();
- try {
- BufferedReader in = null;
- in = new BufferedReader(new InputStreamReader(new FileInputStream(_mfLogCatchFile), "UTF-8"));
- String line = "";
- while ((line = in.readLine()) != null) {
- sb.append(line);
- sb.append("\n");
+ // 日志写入失败时,输出内部调试日志
+ d(TAG, "日志写入失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+ } finally {
+ // 关闭流,避免资源泄漏(Java 7 手动关闭,不使用 try-with-resources)
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
}
- } catch (IOException e) {
- LogUtils.d(TAG, "IOException : " + e.getMessage());
- }
- return sb.toString();
- }
- return "";
- }
-
- //
- // 清理日志函数
- //
- public static void cleanLog() {
- if (_mfLogCatchFile.exists()) {
- try {
- UTF8FileUtils.writeStringToFile(_mfLogCatchFile.getPath(), "");
- //LogUtils.d(TAG, "cleanLog");
- } catch (IOException e) {
- LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
}
+
+ /**
+ * 加载历史日志(读取日志文件所有内容)
+ * @return 历史日志字符串(空字符串表示文件不存在或读取失败)
+ */
+ public static String loadLog() {
+ // 日志文件不存在,返回空
+ if (sLogFile == null || !sLogFile.exists()) {
+ return "";
+ }
+
+ StringBuilder logContent = new StringBuilder();
+ BufferedReader reader = 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");
+ }
+ } catch (IOException e) {
+ // 读取失败时,输出内部调试日志
+ d(TAG, "日志读取失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+ } finally {
+ // 关闭流,避免资源泄漏
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return logContent.toString();
+ }
+
+ /**
+ * 清理历史日志(清空日志文件内容)
+ */
+ public static void cleanLog() {
+ if (sLogFile == null || !sLogFile.exists()) {
+ return;
+ }
+
+ try {
+ // 写入空字符串到文件,实现清空(Java 7 手动处理流)
+ BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(
+ new FileOutputStream(sLogFile),
+ "UTF-8"
+ )
+ );
+ writer.write("");
+ writer.close();
+ } catch (IOException e) {
+ // 清空失败时,输出内部调试日志(带调用栈)
+ d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+
+ /**
+ * 辅助方法:创建文件夹(不存在则创建)
+ * @param dir 目标文件夹
+ */
+ private static void createDirIfNotExists(File dir) {
+ if (dir != null && !dir.exists()) {
+ dir.mkdirs();
+ }
+ }
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java
index 1a9d211f..b8556b96 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java
@@ -1,70 +1,131 @@
package cc.winboll.studio.libappbase;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import java.io.IOException;
+
/**
* @Author ZhanGSKen
* @Date 2024/08/23 15:39:07
- * @Describe LogUtils 数据配置类。
+ * @Describe LogUtils 配置数据模型(继承 BaseBean,实现 JSON 序列化/反序列化)
+ * 封装 LogUtils 的核心配置参数(当前仅日志级别),用于配置的持久化存储与读取
*/
-import android.util.JsonReader;
-import android.util.JsonWriter;
-import java.io.IOException;
public class LogUtilsBean extends BaseBean {
+ /** 当前类的日志 TAG(用于调试输出) */
public static final String TAG = "LogUtilsBean";
- LogUtils.LOG_LEVEL logLevel;
+ /**
+ * 全局日志级别(默认值:Off,即不输出任何日志)
+ * 关联 LogUtils.LOG_LEVEL 枚举,存储日志输出的级别阈值
+ */
+ private LogUtils.LOG_LEVEL logLevel;
+ /**
+ * 无参构造方法(默认初始化日志级别为 Off)
+ * 用于 JSON 反序列化时的实例创建
+ */
public LogUtilsBean() {
this.logLevel = LogUtils.LOG_LEVEL.Off;
}
+ /**
+ * 有参构造方法(指定初始日志级别)
+ * @param logLevel 初始日志级别(如 LogUtils.LOG_LEVEL.Debug)
+ */
public LogUtilsBean(LogUtils.LOG_LEVEL logLevel) {
this.logLevel = logLevel;
}
+ /**
+ * 设置日志级别(更新配置时使用)
+ * @param logLevel 目标日志级别
+ */
public void setLogLevel(LogUtils.LOG_LEVEL logLevel) {
this.logLevel = logLevel;
}
+ /**
+ * 获取当前日志级别(读取配置时使用)
+ * @return 当前配置的日志级别
+ */
public LogUtils.LOG_LEVEL getLogLevel() {
return logLevel;
}
+ /**
+ * 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别)
+ * @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsBean")
+ */
@Override
public String getName() {
return LogUtilsBean.class.getName();
}
+ /**
+ * 重写父类方法:将当前配置对象序列化为 JSON(持久化存储时调用)
+ * 序列化字段:logLevel(存储枚举的 ordinal 值,确保反序列化一致性)
+ * @param jsonWriter JSON 写入器(用于输出 JSON 数据)
+ * @throws IOException JSON 写入异常(如流关闭、格式错误)
+ */
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ // 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理)
super.writeThisToJsonWriter(jsonWriter);
- LogUtilsBean bean = this;
- jsonWriter.name("logLevel").value(bean.getLogLevel().ordinal());
+ // 序列化日志级别:存储枚举的索引值(如 Off=0、Error=1...),比存储名称更高效
+ jsonWriter.name("logLevel").value(this.getLogLevel().ordinal());
}
+ /**
+ * 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用)
+ * 解析字段:logLevel(通过索引值恢复 LogUtils.LOG_LEVEL 枚举)
+ * @param jsonReader JSON 读取器(用于读取 JSON 数据)
+ * @param name JSON 字段名(当前解析的字段)
+ * @return true:字段解析成功;false:字段不匹配(需父类处理或跳过)
+ * @throws IOException JSON 读取异常(如字段类型不匹配、流中断)
+ */
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
- if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
- if (name.equals("logLevel")) {
- setLogLevel(LogUtils.LOG_LEVEL.values()[jsonReader.nextInt()]);
- } else {
- return false;
- }
+ // 先让父类处理公共字段,处理成功则直接返回
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
}
+ // 解析当前类专属字段
+ if ("logLevel".equals(name)) {
+ // 通过枚举索引值恢复枚举实例(确保与序列化时的 ordinal 对应)
+ int levelOrdinal = jsonReader.nextInt();
+ this.setLogLevel(LogUtils.LOG_LEVEL.values()[levelOrdinal]);
+ } else {
+ // 字段不匹配,返回 false 表示需要跳过该字段
+ return false;
+ }
+ // 字段解析成功
return true;
}
+ /**
+ * 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法)
+ * 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理
+ * @param jsonReader JSON 读取器(传入待解析的 JSON 流)
+ * @return 解析后的当前 LogUtilsBean 实例(支持链式调用)
+ * @throws IOException JSON 解析异常(如格式错误、字段缺失)
+ */
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ // 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应)
jsonReader.beginObject();
+ // 遍历 JSON 中的所有字段
while (jsonReader.hasNext()) {
- String name = jsonReader.nextName();
- if (!initObjectsFromJsonReader(jsonReader, name)) {
+ String fieldName = jsonReader.nextName();
+ // 解析字段,若字段不匹配则跳过该值(避免解析失败)
+ if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
jsonReader.skipValue();
}
}
- // 结束 JSON 对象
+ // 结束 JSON 对象解析(必须调用,否则会导致流异常)
jsonReader.endObject();
+ // 返回当前实例,支持链式调用(如 new LogUtilsBean().readBeanFromJsonReader(reader))
return this;
}
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java
index af9c57d4..83ce79e2 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java
@@ -1,87 +1,161 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/01/04 14:17:02
- * @Describe 日志类class TAG 标签数据类
- */
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/01/04 14:17:02
+ * @Describe 日志 TAG 过滤配置模型(继承 BaseBean,实现 JSON 序列化/反序列化)
+ * 封装单个日志 TAG 的名称及其启用状态,用于 LogUtils 的 TAG 过滤规则持久化存储与读取
+ */
public class LogUtilsClassTAGBean extends BaseBean {
+ /** 当前类的日志 TAG(用于调试输出) */
public static final String TAG = "LogUtilsClassTAGBean";
- // 标签名
- String tag;
- // 是否启用
- Boolean enable;
+ /**
+ * 日志 TAG 名称(如 "LogViewThread"、"ToastUtils")
+ * 与 LogUtils 中扫描的应用内 TAG 一一对应
+ */
+ private String tag;
+ /**
+ * TAG 启用状态(控制该 TAG 的日志是否输出)
+ * true:启用(输出该 TAG 的日志);false:禁用(不输出该 TAG 的日志)
+ */
+ private Boolean enable;
+
+ /**
+ * 无参构造方法(默认初始化:TAG 为当前类 TAG,启用状态为 true)
+ * 用于 JSON 反序列化时的实例创建,或默认配置生成
+ */
public LogUtilsClassTAGBean() {
- this.tag = TAG;
- this.enable = true;
+ this.tag = TAG; // 默认 TAG 为当前类的 TAG
+ this.enable = true; // 默认启用该 TAG 的日志输出
}
+ /**
+ * 有参构造方法(指定 TAG 名称和启用状态)
+ * 用于主动创建 TAG 过滤配置实例
+ * @param tag 日志 TAG 名称
+ * @param enable TAG 启用状态(true/false)
+ */
public LogUtilsClassTAGBean(String tag, Boolean enable) {
this.tag = tag;
this.enable = enable;
}
+ /**
+ * 设置日志 TAG 名称
+ * @param tag 目标 TAG 名称
+ */
public void setTag(String tag) {
this.tag = tag;
}
+ /**
+ * 获取日志 TAG 名称
+ * @return 当前配置的 TAG 名称
+ */
public String getTag() {
return tag;
}
+ /**
+ * 设置 TAG 启用状态
+ * @param enable 目标启用状态(true:启用;false:禁用)
+ */
public void setEnable(Boolean enable) {
this.enable = enable;
}
+ /**
+ * 获取 TAG 启用状态
+ * @return 当前 TAG 的启用状态
+ */
public Boolean getEnable() {
return enable;
}
+ /**
+ * 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别)
+ * @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsClassTAGBean")
+ */
@Override
public String getName() {
return LogUtilsClassTAGBean.class.getName();
}
+ /**
+ * 重写父类方法:将当前 TAG 配置对象序列化为 JSON(持久化存储时调用)
+ * 序列化字段:tag(TAG 名称)、enable(启用状态)
+ * @param jsonWriter JSON 写入器(用于输出 JSON 数据)
+ * @throws IOException JSON 写入异常(如流关闭、格式错误)
+ */
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ // 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理)
super.writeThisToJsonWriter(jsonWriter);
- LogUtilsClassTAGBean bean = this;
- jsonWriter.name("tag").value(bean.getTag());
- jsonWriter.name("enable").value(bean.getEnable());
+ // 序列化 TAG 名称
+ jsonWriter.name("tag").value(this.getTag());
+ // 序列化启用状态
+ jsonWriter.name("enable").value(this.getEnable());
}
+ /**
+ * 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用)
+ * 解析字段:tag(TAG 名称)、enable(启用状态)
+ * @param jsonReader JSON 读取器(用于读取 JSON 数据)
+ * @param name JSON 字段名(当前解析的字段)
+ * @return true:字段解析成功;false:字段不匹配(需父类处理或跳过)
+ * @throws IOException JSON 读取异常(如字段类型不匹配、流中断)
+ */
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
- if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
- if (name.equals("tag")) {
- setTag(jsonReader.nextString());
- } else if (name.equals("enable")) {
- setEnable(jsonReader.nextBoolean());
- } else {
- return false;
- }
+ // 先让父类处理公共字段,处理成功则直接返回
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
}
+ // 解析当前类专属字段
+ if ("tag".equals(name)) {
+ // 读取 TAG 名称并设置
+ this.setTag(jsonReader.nextString());
+ } else if ("enable".equals(name)) {
+ // 读取启用状态并设置
+ this.setEnable(jsonReader.nextBoolean());
+ } else {
+ // 字段不匹配,返回 false 表示需要跳过该字段
+ return false;
+ }
+ // 字段解析成功
return true;
}
+ /**
+ * 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法)
+ * 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理
+ * @param jsonReader JSON 读取器(传入待解析的 JSON 流)
+ * @return 解析后的当前 LogUtilsClassTAGBean 实例(支持链式调用)
+ * @throws IOException JSON 解析异常(如格式错误、字段缺失)
+ */
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ // 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应)
jsonReader.beginObject();
+ // 遍历 JSON 中的所有字段
while (jsonReader.hasNext()) {
- String name = jsonReader.nextName();
- if (!initObjectsFromJsonReader(jsonReader, name)) {
+ String fieldName = jsonReader.nextName();
+ // 解析字段,若字段不匹配则跳过该值(避免解析失败)
+ if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
jsonReader.skipValue();
}
}
- // 结束 JSON 对象
+ // 结束 JSON 对象解析(必须调用,否则会导致流异常)
jsonReader.endObject();
+ // 返回当前实例,支持链式调用(如 new LogUtilsClassTAGBean().readBeanFromJsonReader(reader))
return this;
}
}
+
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 61dd87b2..af50d0fa 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2024/08/12 14:36:18
- * @Describe 日志视图类,继承 RelativeLayout 类。
- */
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -26,8 +21,6 @@ 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 java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -35,27 +28,49 @@ 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";
- public volatile boolean mIsHandling;
- public volatile boolean mIsAddNewLog;
+ /** 日志处理中标志(避免并发刷新,volatile 保证多线程可见性) */
+ private volatile boolean mIsHandling;
+ /** 新日志添加标志(标记有未处理的新日志,volatile 保证多线程可见性) */
+ private volatile boolean mIsAddNewLog;
- Context mContext;
- ScrollView mScrollView;
- TextView mTextView;
- EditText metTagSearch;
- CheckBox mSelectableCheckBox;
- CheckBox mSelectAllTAGCheckBox;
- TAGListAdapter mTAGListAdapter;
- LogViewThread mLogViewThread;
- LogViewHandler mLogViewHandler;
- Spinner mLogLevelSpinner;
- ArrayAdapter mLogLevelSpinnerAdapter;
- // 标签列表
- HorizontalListView mListViewTags;
+ /** 上下文对象(用于布局加载、系统服务获取) */
+ 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;
+ // ====================== 构造方法(初始化视图) ======================
public LogView(Context context) {
super(context);
initView(context);
@@ -76,258 +91,307 @@ public class LogView extends RelativeLayout {
initView(context);
}
+ /**
+ * 启动日志监听与展示
+ * 1. 初始化并启动 LogViewThread(监听日志文件变化);
+ * 2. 初始加载并展示日志内容。
+ */
public void start() {
- mLogViewThread = new LogViewThread(LogView.this);
+ mLogViewThread = new LogViewThread(this);
mLogViewThread.start();
- // 显示日志
- showAndScrollLogView();
+ showAndScrollLogView(); // 初始显示日志并滚动到底部
}
- 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);
- }
- }
- });
+ /**
+ * 滚动日志到底部(确保最新日志可见)
+ * 运行在主线程,通过 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);
+ }
+ }
+ });
}
- void initView(Context context) {
+ /**
+ * 初始化视图组件(加载布局、绑定控件、设置监听)
+ * @param context 上下文对象
+ */
+ private void initView(Context context) {
mContext = context;
- 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);
+ mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler
- metTagSearch.addTextChangedListener(new TextWatcher() {
+ // 加载日志视图布局(R.layout.view_log 为自定义布局文件)
+ View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true);
+ // 绑定布局控件(通过 ID 找到对应组件)
+ bindViews(rootView);
- @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()));
- }
- });
-
-
- // 设置滚动时不聚焦日志
+ // 设置 TAG 搜索输入框监听(实时搜索并定位 TAG)
+ setupTagSearchListener();
+ // 设置功能按钮监听(清理日志、复制日志)
+ setupFunctionButtonListeners(rootView);
+ // 设置文本选择开关监听(控制日志文本是否可选中)
+ setupTextSelectableListener();
+ // 初始化日志级别下拉框(绑定级别数据,设置默认值)
+ initLogLevelSpinner();
+ // 初始化 TAG 列表(加载所有 TAG,设置全选状态)
+ initTagListView();
+ // 设置默认交互模式(默认禁止子视图获取焦点,避免误触)
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() == true) {
- // 正在处理日志显示,
- // 就先设置一个新日志标志位
- // 以便日志显示完后,再次显示新日志内容
+ if (mLogViewHandler.isHandling()) {
+ // 正在处理日志刷新,标记有新日志待处理
mLogViewHandler.setIsAddNewLog(true);
} else {
- //LogUtils.d(TAG, "LogListener showLog(String path)");
- Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
- mLogViewHandler.sendMessage(message);
+ // 发送刷新消息到主线程
+ Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
+ mLogViewHandler.sendMessage(refreshMsg);
mLogViewHandler.setIsAddNewLog(false);
}
}
- void showAndScrollLogView() {
- mTextView.setText(LogUtils.loadLog());
- scrollLogUp();
+ /**
+ * 显示日志并滚动到底部
+ * 1. 从 LogUtils 加载所有历史日志;
+ * 2. 设置到文本控件并滚动到底部。
+ */
+ private void showAndScrollLogView() {
+ mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志
+ scrollLogToBottom(); // 滚动到底部,显示最新日志
}
- public void scrollToTag(final String prefix) {
- if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) {
- LogUtils.d(TAG, "参数为空,无法滚动");
+ /**
+ * 滚动到目标 TAG(根据搜索文本定位匹配的 TAG 并滚动显示)
+ * @param prefix 搜索文本(TAG 前缀)
+ */
+ private void scrollToTargetTag(final String prefix) {
+ if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) {
+ LogUtils.d(TAG, "TAG 搜索参数为空,无法定位");
return;
}
- final List itemList = mTAGListAdapter.getItemList();
+ 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;
+ }
+ }
- 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);
- }
- }
- });
+ 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);
+ }
+ }
+ });
}
-
-
- class LogViewHandler extends Handler {
-
- final static int MSG_LOGVIEW_UPDATE = 0;
- volatile boolean isHandling;
- volatile boolean isAddNewLog;
+ // ====================== 内部类:日志视图 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;
public LogViewHandler() {
setIsHandling(false);
@@ -350,24 +414,32 @@ public class LogView extends RelativeLayout {
return isAddNewLog;
}
+ @Override
public void handleMessage(Message msg) {
+ super.handleMessage(msg);
switch (msg.what) {
- case MSG_LOGVIEW_UPDATE:{
- if (isHandling() == false) {
- setIsHandling(true);
- showAndScrollLogView();
- }
- break;
+ case MSG_LOG_REFRESH:
+ // 未处理日志刷新时,标记为处理中并触发显示
+ if (!isHandling()) {
+ setIsHandling(true);
+ showAndScrollLogView();
}
+ break;
default:
break;
}
- super.handleMessage(msg);
}
}
- public class TAGItemModel {
+ // ====================== 内部类:TAG 数据模型(封装 TAG 名称与状态) ======================
+ /**
+ * TAG 列表项数据模型
+ * 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定)
+ */
+ private class TAGItemModel {
+ /** TAG 名称(如 "LogViewThread"、"LogUtils") */
private String tag;
+ /** TAG 启用状态(true:启用;false:禁用) */
private boolean isChecked;
public TAGItemModel(String tag, boolean isChecked) {
@@ -391,18 +463,17 @@ public class LogView extends RelativeLayout {
isChecked = checked;
}
- // getter/setter...
-
+ /**
+ * 重写 equals 方法(按 TAG 名称判断相等)
+ * @param o 比较对象
+ * @return true:TAG 名称相同;false:不同
+ */
@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 6 不支持 Objects.equals)
+ // 手动处理空值比较(兼容 Java 7,不依赖 Objects.equals)
if (tag == null) {
return that.tag == null;
} else {
@@ -410,106 +481,174 @@ 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();
}
}
-
- public class TAGListAdapter extends BaseAdapter {
-
+ // ====================== 内部类:TAG 列表适配器(绑定数据与视图) ======================
+ /**
+ * TAG 水平列表适配器(继承 BaseAdapter)
+ * 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
+ */
+ private class TAGListAdapter extends BaseAdapter {
+ /** 上下文对象(用于加载列表项布局) */
private Context context;
- private Map mapOrigin;
- private List itemList;
+ /** 原始 TAG 启用状态映射表(来自 LogUtils) */
+ private Map originTagMap;
+ /** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
+ private List tagItemList;
- public TAGListAdapter(Context context, Map map) {
+ /**
+ * 构造方法(初始化数据并加载到列表)
+ * @param context 上下文
+ * @param tagMap TAG 启用状态映射表
+ */
+ public TAGListAdapter(Context context, Map tagMap) {
this.context = context;
- mapOrigin = map;
- loadMap(mapOrigin);
+ this.originTagMap = tagMap;
+ loadTagData(originTagMap); // 加载并转换数据
}
+ /**
+ * 获取 TAG 列表项数据(供外部定位 TAG 使用)
+ * @return TAGItemModel 列表
+ */
public List getItemList() {
- return itemList;
+ return tagItemList;
}
+ // ====================== BaseAdapter 抽象方法实现 ======================
@Override
public int getCount() {
- return itemList.size();
+ return tagItemList == null ? 0 : tagItemList.size();
}
@Override
- public Object getItem(int p) {
- return itemList.get(p);
+ public Object getItem(int position) {
+ return tagItemList.get(position);
}
@Override
- public long getItemId(int p) {
- return p;
+ public long getItemId(int position) {
+ return position;
}
- void loadMap(Map map) {
- itemList = new ArrayList();
- for (Map.Entry entry : map.entrySet()) {
- itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
+ /**
+ * 加载 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()));
}
- // 添加排序功能,按照tag进行升序排序
- Collections.sort(itemList, new SortMapEntryByKeyString(true));
- //Collections.sort(itemList, new SortMapEntryByKeyString(false));
+ // 按 TAG 名称升序排序(中文排序兼容)
+ Collections.sort(tagItemList, new TagAscComparator(true));
}
+ /**
+ * 重新加载 TAG 数据(用于全选/反选后刷新列表)
+ */
public void reload() {
- loadMap(mapOrigin);
- super.notifyDataSetChanged();
+ loadTagData(originTagMap); // 重新加载数据
+ 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();
- holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
- holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
- convertView.setTag(holder);
+ // 绑定列表项控件(TAG 文本和勾选框)
+ holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1);
+ holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1);
+ convertView.setTag(holder); // 保存 ViewHolder 到视图
} else {
- holder = (ViewHolder) convertView.getTag();
+ holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder
}
- final TAGItemModel item = itemList.get(position);
- holder.tvText.setText(item.getTag());
- holder.cbChecked.setChecked(item.isChecked());
- holder.cbChecked.setOnClickListener(new View.OnClickListener(){
+ // 绑定数据到视图
+ final TAGItemModel item = tagItemList.get(position);
+ holder.tagTv.setText(item.getTag()); // 设置 TAG 名称
+ holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态
- @Override
- public void onClick(View v) {
- LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
- }
- });
+ // 勾选框点击监听(更新 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);
+ }
+ });
return convertView;
}
- public class ViewHolder {
- TextView tvText;
- CheckBox cbChecked;
+ /**
+ * 列表项 ViewHolder(缓存控件,提升列表滑动性能)
+ */
+ private class ViewHolder {
+ TextView tagTv; // TAG 名称文本控件
+ CheckBox tagCb; // TAG 启用状态勾选框
}
}
- class SortMapEntryByKeyString implements Comparator {
- private boolean mIsDesc = true;
- // isDesc 是否降序排列
- public SortMapEntryByKeyString(boolean isDesc) {
- mIsDesc = isDesc;
+ // ====================== 内部类: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;
}
- Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
+
+ /**
+ * 比较两个 TAGItemModel(按 TAG 名称排序)
+ * @param o1 第一个比较对象
+ * @param o2 第二个比较对象
+ * @return 比较结果(正数:o1 在 o2 后;负数:o1 在 o2 前;0:相等)
+ */
@Override
public int compare(TAGItemModel o1, TAGItemModel o2) {
- if (mIsDesc) {
- return o1.getTag().compareTo(o2.getTag());
+ 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); // 升序
} else {
- return o2.getTag().compareTo(o1.getTag());
+ return chineseCollator.compare(tag2, tag1); // 降序
}
}
}
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java
index bccbe035..3941e456 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java
@@ -1,80 +1,140 @@
package cc.winboll.studio.libappbase;
+import android.os.FileObserver;
+import java.lang.ref.WeakReference;
+
/**
* @Author ZhanGSKen
* @Date 2024/08/12 14:43:50
* @Describe 日志视图线程类
+ * 独立线程监听日志文件目录变化(如写入、删除),触发日志视图更新,避免阻塞主线程
*/
-import android.os.FileObserver;
-import cc.winboll.studio.libappbase.LogUtils;
-import java.lang.ref.WeakReference;
-
public class LogViewThread extends Thread {
+ /** 日志标签(用于调试输出) */
public static final String TAG = "LogViewThread";
- // 线程退出标志
- volatile boolean isExist = false;
- // 应用日志文件监听实例
- LogListener mLogListener;
- // 日志视图弱引用
- WeakReference mwrLogView;
+ /** 线程退出标志(volatile 保证多线程可见性,控制循环退出) */
+ private volatile boolean isExit = false;
+ /** 日志文件目录监听实例(监听文件写入、删除事件) */
+ private LogListener mLogListener;
+ /** 日志视图弱引用(避免持有 LogView 强引用导致内存泄漏) */
+ private final WeakReference mLogViewWeakRef;
- //
- // 构造函数
- // @logView : 日志显示输出视图类
+ /**
+ * 构造函数
+ * @param logView 日志显示视图实例(需通过弱引用持有,避免内存泄漏)
+ */
public LogViewThread(LogView logView) {
- mwrLogView = new WeakReference(logView);
-
+ // 使用弱引用包装 LogView,当视图销毁时可被 GC 回收
+ mLogViewWeakRef = new WeakReference<>(logView);
}
- public void setIsExist(boolean isExist) {
- this.isExist = isExist;
+ /**
+ * 设置线程退出标志(触发线程停止监听并退出)
+ * @param exit true:退出线程;false:继续运行(默认)
+ */
+ public void setExit(boolean exit) {
+ this.isExit = exit;
}
- public boolean isExist() {
- return isExist;
+ /**
+ * 获取当前线程退出状态
+ * @return true:已标记退出;false:运行中
+ */
+ public boolean isExit() {
+ return isExit;
}
+ /**
+ * 线程核心逻辑:初始化文件监听并启动循环,直到收到退出标志
+ */
@Override
public void run() {
- String szLogDir = LogUtils.getLogCacheDir().getPath();
- mLogListener = new LogListener(szLogDir);
+ // 获取日志缓存目录路径(从 LogUtils 统一获取,确保路径一致性)
+ String logDirPath = LogUtils.getLogCacheDir().getPath();
+ LogUtils.d(TAG, "启动日志文件监听,监听目录:" + logDirPath);
+
+ // 初始化日志文件监听器(监听目标目录的文件事件)
+ mLogListener = new LogListener(logDirPath);
+ // 开始监听文件事件(非阻塞,内部通过 Native 层实现)
mLogListener.startWatching();
- while (isExist() == false) {
+
+ // 循环等待退出标志(每 1 秒检查一次,降低 CPU 占用)
+ while (!isExit()) {
try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {}
+ Thread.sleep(1000); // 休眠 1 秒,避免忙等
+ } catch (InterruptedException e) {
+ // 线程被中断时,恢复中断标志并退出循环(避免无限阻塞)
+ Thread.currentThread().interrupt();
+ LogUtils.d(TAG, "日志监听线程被中断,准备退出。" + e);
+ break;
+ }
}
+
+ // 收到退出标志,停止监听并释放资源
mLogListener.stopWatching();
+ LogUtils.d(TAG, "日志文件监听已停止,线程退出");
}
+ /**
+ * 日志文件监听内部类(继承 FileObserver,监听目录下文件变化)
+ * 仅关注文件写入完成(CLOSE_WRITE)和文件删除(DELETE)事件
+ */
+ private class LogListener extends FileObserver {
- //
- // 日志文件监听类
- //
- class LogListener extends FileObserver {
+ /**
+ * 构造函数
+ * @param path 监听的目录路径(此处为日志缓存目录)
+ */
public LogListener(String path) {
+ // 父类构造:监听指定目录的所有事件(通过位掩码 ALL_EVENTS 指定)
super(path);
}
+ /**
+ * 文件事件回调(运行在系统私有线程,非主线程)
+ * @param event 事件类型(通过位掩码表示,需与 ALL_EVENTS 按位与解析)
+ * @param path 发生事件的文件名(相对监听目录的路径)
+ */
@Override
public void onEvent(int event, String path) {
- int e = event & FileObserver.ALL_EVENTS;
- switch (e) {
- case FileObserver.CLOSE_WRITE:{
- if (mwrLogView.get() != null) {
- mwrLogView.get().updateLogView();
- }
- break;
- }
- case FileObserver.DELETE:{
- if (mwrLogView.get() != null) {
- mwrLogView.get().updateLogView();
- }
- break;
- }
+ // 解析事件类型(排除无关事件,只处理目标事件)
+ int eventType = event & FileObserver.ALL_EVENTS;
+
+ switch (eventType) {
+ // 事件:文件写入完成(如日志写入结束并关闭文件)
+ case FileObserver.CLOSE_WRITE:
+ // 触发日志视图更新(需先判断 LogView 是否未被回收)
+ updateLogView();
+ break;
+
+ // 事件:文件被删除(如日志清理操作)
+ case FileObserver.DELETE:
+ LogUtils.d(TAG, "日志文件被删除,文件名:" + (path != null ? path : "未知"));
+ // 触发日志视图更新(刷新视图显示空状态)
+ updateLogView();
+ break;
+
+ default:
+ // 忽略其他无关事件(如文件创建、访问等)
+ break;
+ }
+ }
+
+ /**
+ * 触发日志视图更新(通过弱引用获取 LogView,避免内存泄漏)
+ */
+ private void updateLogView() {
+ // 从弱引用中获取 LogView 实例(若视图已销毁,get() 返回 null)
+ LogView logView = mLogViewWeakRef.get();
+ if (logView != null) {
+ // 调用 LogView 的更新方法(需确保 updateLogView 内部处理主线程切换)
+ logView.updateLogView();
+ } else {
+ LogUtils.w(TAG, "LogView 已被回收,无法更新日志视图");
}
}
}
}
+
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 a22f2639..f4586ff2 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java
@@ -1,34 +1,257 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2025/03/12 12:02:31
- */
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.widget.Toast;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:51
+ * @Describe 吐司工具类(单例模式)
+ * 简化 Android 吐司的创建与展示,通过独立线程 + Handler 处理消息,最终切换到主线程显示吐司,避免内存泄漏
+ */
public class ToastUtils {
+ /** 工具类日志 TAG(用于调试输出) */
public static final String TAG = "ToastUtils";
+ /** 消息标识:显示短时长吐司 */
+ private static final int MSG_SHOW_SHORT_TOAST = 1001;
- volatile static ToastUtils _ToastUtils;
- Context mContext;
+ /** 单例实例(volatile 保证多线程下可见性,避免指令重排) */
+ private static volatile ToastUtils sInstance;
+ /** 全局上下文(volatile 保证多线程可见性,避免空指针) */
+ private volatile Context mContext;
+ /** 独立线程的 Handler(volatile 保证可见性) */
+ private volatile Handler mWorkerHandler;
+ /** 主线程 Handler(volatile 保证可见性) */
+ private volatile Handler mMainHandler;
+ /** 消息处理独立线程 */
+ private Thread mWorkerThread;
+ /** 资源释放标记(volatile 避免多线程误操作) */
+ private volatile boolean isReleased = false;
- ToastUtils() {
+ /**
+ * 私有构造方法(禁止外部直接创建实例,确保单例)
+ * 1. 初始化主线程 Handler;
+ * 2. 创建并启动独立消息处理线程。
+ */
+ private ToastUtils() {
+ initMainHandler(); // 优先初始化主线程 Handler
+ startWorkerThread(); // 启动独立消息处理线程
}
- synchronized static ToastUtils getInstance() {
- if (_ToastUtils == null) {
- _ToastUtils = new ToastUtils();
+ /**
+ * 初始化主线程 Handler
+ */
+ private void initMainHandler() {
+ if (Looper.getMainLooper() == null) {
+ LogUtils.e(TAG, "主线程 Looper 为空,无法初始化 mMainHandler");
+ throw new IllegalStateException("主线程 Looper 未初始化,无法创建 ToastUtils");
}
- return _ToastUtils;
+ mMainHandler = new Handler(Looper.getMainLooper());
+ LogUtils.d(TAG, "主线程 Handler 初始化完成,线程ID:" + Looper.getMainLooper().getThread().getId());
}
+ /**
+ * 启动独立消息处理线程
+ */
+ private void startWorkerThread() {
+ mWorkerThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ LogUtils.d(TAG, "消息处理线程启动,线程ID:" + Thread.currentThread().getId());
+ Looper.prepare();
+
+ mWorkerHandler = new Handler(Looper.myLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ // 若已释放,直接返回,不处理消息
+ if (isReleased) {
+ LogUtils.w(TAG, "资源已释放,忽略消息处理");
+ return;
+ }
+ LogUtils.d(TAG, "WorkerHandler 接收消息,当前线程ID:" + Thread.currentThread().getId());
+ if (msg.what == MSG_SHOW_SHORT_TOAST && msg.obj != null) {
+ String message = (String) msg.obj;
+ postToMainThreadShowToast(message);
+ }
+ }
+ };
+
+ Looper.loop();
+ LogUtils.d(TAG, "消息处理线程退出");
+ }
+ }, "ToastWorkerThread");
+ mWorkerThread.start();
+ }
+
+ /**
+ * 获取单例实例(双重检查锁定)
+ * @return ToastUtils 单例对象
+ */
+ private static ToastUtils getInstance() {
+ if (sInstance == null) {
+ synchronized (ToastUtils.class) {
+ if (sInstance == null) {
+ sInstance = new ToastUtils();
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ /**
+ * 初始化工具类(必须在 Application 启动时调用)
+ * @param context 全局上下文(推荐 Application 上下文)
+ */
public static void init(Context context) {
- getInstance().mContext = context;
+ if (context == null) {
+ throw new IllegalArgumentException("初始化上下文不能为 null!");
+ }
+ ToastUtils instance = getInstance();
+ // 若已释放,重置释放标记
+ if (instance.isReleased) {
+ instance.isReleased = false;
+ instance.startWorkerThread(); // 重新启动线程
+ }
+ instance.mContext = context.getApplicationContext();
+ LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
}
+ /**
+ * 外部接口:显示短时长吐司
+ * @param message 吐司内容
+ */
public static void show(String message) {
- Toast.makeText(getInstance().mContext, message, Toast.LENGTH_SHORT).show();
+ LogUtils.d(TAG, "外部调用 show(),当前线程ID:" + Thread.currentThread().getId());
+ if (message == null || message.isEmpty()) {
+ return;
+ }
+
+ ToastUtils instance = getInstance();
+ // 校验资源是否已释放
+ if (instance.isReleased) {
+ LogUtils.w(TAG, "ToastUtils 已释放,无法显示吐司:" + message);
+ return;
+ }
+ // 校验上下文是否初始化
+ if (instance.mContext == null) {
+ LogUtils.e(TAG, "ToastUtils 未初始化!请先调用 init(Context) 方法");
+ // 不抛出异常,避免崩溃,改为日志提示
+ return;
+ }
+
+ instance.sendToastMessage(message);
+ }
+
+ /**
+ * 发送吐司消息到 WorkerHandler
+ * @param message 吐司内容
+ */
+ private void sendToastMessage(String message) {
+ LogUtils.d(TAG, "发送消息到 WorkerHandler");
+ // 校验 WorkerHandler 是否就绪
+ if (mWorkerHandler == null) {
+ LogUtils.w(TAG, "WorkerHandler 未就绪,直接主线程显示");
+ postToMainThreadShowToast(message);
+ return;
+ }
+ // 发送消息
+ Message msg = mWorkerHandler.obtainMessage(MSG_SHOW_SHORT_TOAST);
+ msg.obj = message;
+ mWorkerHandler.sendMessage(msg);
+ }
+
+ /**
+ * 切换到主线程显示吐司
+ * @param message 吐司内容
+ */
+ private void postToMainThreadShowToast(final String message) {
+ LogUtils.d(TAG, "切换到主线程显示吐司,当前线程ID:" + Thread.currentThread().getId());
+ // 校验资源是否已释放
+ if (isReleased) {
+ LogUtils.w(TAG, "资源已释放,取消显示吐司");
+ return;
+ }
+ // 校验并初始化 mMainHandler
+ if (mMainHandler == null) {
+ LogUtils.e(TAG, "mMainHandler 为空,尝试重新初始化");
+ initMainHandler();
+ if (mMainHandler == null) {
+ LogUtils.e(TAG, "mMainHandler 初始化失败,无法显示吐司:" + message);
+ return;
+ }
+ }
+ // 主线程显示
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (isReleased) return; // 释放后取消执行
+ showToastInternal(message);
+ }
+ });
+ }
+
+ /**
+ * 实际显示吐司(主线程)
+ * @param message 吐司内容
+ */
+ private void showToastInternal(String message) {
+ LogUtils.d(TAG, "执行 showToastInternal()");
+ // 最终校验上下文
+ if (mContext == null) {
+ LogUtils.w(TAG, "上下文为空,无法显示吐司:" + message);
+ // 尝试重新获取 Application 上下文(降级策略)
+ Context appContext = GlobalApplication.getInstance();
+ if (appContext != null) {
+ mContext = appContext;
+ Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+ LogUtils.d(TAG, "通过 GlobalApplication 获取上下文,成功显示吐司");
+ }
+ return;
+ }
+ Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * 释放资源(仅在应用退出时调用)
+ */
+ public static void release() {
+ LogUtils.d(TAG, "开始释放 ToastUtils 资源");
+ ToastUtils instance = getInstance();
+ // 标记为已释放,阻止后续消息处理
+ instance.isReleased = true;
+
+ // 停止 Worker 线程
+ if (instance.mWorkerHandler != null && instance.mWorkerHandler.getLooper() != null) {
+ instance.mWorkerHandler.getLooper().quit();
+ instance.mWorkerHandler = null;
+ }
+
+ // 清理主线程 Handler
+ if (instance.mMainHandler != null) {
+ instance.mMainHandler.removeCallbacksAndMessages(null);
+ instance.mMainHandler = null;
+ }
+
+ // 等待线程退出
+ if (instance.mWorkerThread != null && instance.mWorkerThread.isAlive()) {
+ try {
+ instance.mWorkerThread.join(1000);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ //LogUtils.e(TAG, "线程退出异常", e);
+ Thread.currentThread().interrupt();
+ }
+ instance.mWorkerThread = null;
+ }
+
+ // 清空上下文(避免内存泄漏)
+ instance.mContext = null;
+ LogUtils.d(TAG, "ToastUtils 资源释放完成");
}
}
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java
index 5c797fd7..c7407fb5 100644
--- a/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java
@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
-/**
- * @Author ZhanGSKen
- * @Date 2024/07/19 14:30:57
- * @Describe UTF-8编码文件工具类
- */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -13,37 +8,79 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/11 20:45
+ * @Describe UTF-8 编码文件操作工具类
+ * 提供字符串与文件的相互转换,强制使用 UTF-8 编码,确保跨平台字符兼容性
+ */
public class UTF8FileUtils {
- public static final String TAG = "FileUtils";
+ /** 工具类日志 TAG(用于调试输出) */
+ public static final String TAG = "UTF8FileUtils";
- //
- // 把字符串写入文件,指定 UTF-8 编码
- //
- public static void writeStringToFile(String szFilePath, String szContent) throws IOException {
- File file = new File(szFilePath);
- if (!file.getParentFile().exists()) {
- file.getParentFile().mkdirs();
+ /**
+ * 将字符串写入文件(强制 UTF-8 编码)
+ * 若文件父目录不存在,自动创建;覆盖原有文件内容
+ * @param filePath 文件路径(包含文件名,如 "/sdcard/test.txt")
+ * @param content 要写入的字符串内容
+ * @throws IOException 写入失败时抛出(如权限不足、路径无效等)
+ */
+ public static void writeStringToFile(String filePath, String content) throws IOException {
+ // 根据路径创建文件对象
+ File file = new File(filePath);
+ // 获取父目录,若不存在则递归创建
+ File parentDir = file.getParentFile();
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs();
}
+
+ // 初始化文件输出流(覆盖模式)
FileOutputStream outputStream = new FileOutputStream(file);
+ // 包装为 UTF-8 编码的字符输出流
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
- writer.write(szContent);
- writer.close();
+
+ try {
+ // 写入字符串内容
+ writer.write(content);
+ } finally {
+ // 强制关闭流,避免资源泄漏(即使写入失败也确保流关闭)
+ writer.close();
+ }
}
- //
- // 读取文件到字符串,指定 UTF-8 编码
- //
- public static String readStringFromFile(String szFilePath) throws IOException {
- File file = new File(szFilePath);
+ /**
+ * 从文件读取字符串(强制 UTF-8 编码)
+ * 逐字符读取文件内容,拼接为完整字符串返回
+ * @param filePath 文件路径(包含文件名,如 "/sdcard/test.txt")
+ * @return 文件内容字符串(空文件返回空字符串)
+ * @throws IOException 读取失败时抛出(如文件不存在、权限不足等)
+ */
+ public static String readStringFromFile(String filePath) throws IOException {
+ // 根据路径创建文件对象
+ File file = new File(filePath);
+ // 初始化文件输入流
FileInputStream inputStream = new FileInputStream(file);
+ // 包装为 UTF-8 编码的字符输入流
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+
+ // 字符串构建器,用于拼接读取的字符
StringBuilder content = new StringBuilder();
- int character;
- while ((character = reader.read()) != -1) {
- content.append((char) character);
+ int charCode; // 存储单个字符的 ASCII 码
+
+ try {
+ // 逐字符读取(-1 表示读取到文件末尾)
+ while ((charCode = reader.read()) != -1) {
+ // 将 ASCII 码转换为字符,追加到字符串
+ content.append((char) charCode);
+ }
+ } finally {
+ // 强制关闭流,避免资源泄漏
+ reader.close();
}
- reader.close();
+
+ // 返回读取的完整字符串
return content.toString();
}
}
+
diff --git a/libappbase/src/main/res/values/strings.xml b/libappbase/src/main/res/values/strings.xml
index 5c133057..99fef9df 100644
--- a/libappbase/src/main/res/values/strings.xml
+++ b/libappbase/src/main/res/values/strings.xml
@@ -2,6 +2,5 @@
libappbase
- Hello world!
- cc.winboll.studio.libappbase.action.SOS
+ Hello, world!
diff --git a/positions/.gitignore b/positions/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/positions/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/positions/README.md b/positions/README.md
new file mode 100644
index 00000000..ae1cc396
--- /dev/null
+++ b/positions/README.md
@@ -0,0 +1,34 @@
+# Positions
+
+#### 介绍
+安卓位置应用,有关于地理位置的相关应用。
+
+#### 软件架构
+适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
+也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
+
+
+#### Gradle 编译说明
+调试版编译命令 :gradle assembleBetaDebug
+阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh positions
+
+#### 使用说明
+
+#### 参与贡献
+
+1. Fork 本仓库
+2. 新建 Feat_xxx 分支
+3. 提交代码 : ZhanGSKen(ZhanGSKen)
+4. 新建 Pull Request
+
+
+#### 特技
+
+1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
+3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
+4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
+5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+
+#### 参考文档
diff --git a/positions/app_update_description.txt b/positions/app_update_description.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/positions/app_update_description.txt
@@ -0,0 +1 @@
+
diff --git a/positions/build.gradle b/positions/build.gradle
new file mode 100644
index 00000000..356a27ae
--- /dev/null
+++ b/positions/build.gradle
@@ -0,0 +1,76 @@
+apply plugin: 'com.android.application'
+apply from: '../.winboll/winboll_app_build.gradle'
+apply from: '../.winboll/winboll_lint_build.gradle'
+
+def genVersionName(def versionName){
+ // 检查编译标志位配置
+ assert (winbollBuildProps['stageCount'] != null)
+ assert (winbollBuildProps['baseVersion'] != null)
+ // 保存基础版本号
+ winbollBuildProps.setProperty("baseVersion", "${versionName}");
+ //保存编译标志配置
+ FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
+ winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
+ fos.close();
+
+ // 返回编译版本号
+ return "${versionName}." + winbollBuildProps['stageCount']
+}
+
+android {
+ compileSdkVersion 32
+ buildToolsVersion "32.0.0"
+
+ defaultConfig {
+ applicationId "cc.winboll.studio.positions"
+ minSdkVersion 24
+ targetSdkVersion 30
+ versionCode 1
+ // versionName 更新后需要手动设置
+ // .winboll/winbollBuildProps.properties 文件的 stageCount=0
+ // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
+ versionName "15.0"
+ if(true) {
+ versionName = genVersionName("${versionName}")
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ api fileTree(dir: 'libs', include: ['*.jar'])
+
+ // 谷歌定位服务核心依赖(FusedLocationProviderClient所在库)
+ api 'com.google.android.gms:play-services-location:21.0.1'
+
+ // SSH
+ api 'com.jcraft:jsch:0.1.55'
+ // Html 解析
+ api 'org.jsoup:jsoup:1.13.1'
+ // 二维码类库
+ api 'com.google.zxing:core:3.4.1'
+ api 'com.journeyapps:zxing-android-embedded:3.6.0'
+ // 应用介绍页类库
+ api 'io.github.medyo:android-about-page:2.0.0'
+ // 吐司类库
+ api 'com.github.getActivity:ToastUtils:10.5'
+ // 网络连接类库
+ api 'com.squareup.okhttp3:okhttp:4.4.1'
+ // AndroidX 类库
+ api 'androidx.appcompat:appcompat:1.1.0'
+ api 'com.google.android.material:material:1.4.0'
+ //api 'androidx.viewpager:viewpager:1.0.0'
+ //api 'androidx.vectordrawable:vectordrawable:1.1.0'
+ //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
+ //api 'androidx.fragment:fragment:1.1.0'
+
+ api 'cc.winboll.studio:libaes:15.10.2'
+ api 'cc.winboll.studio:libapputils:15.10.2'
+ api 'cc.winboll.studio:libappbase:15.10.9'
+}
diff --git a/positions/build.properties b/positions/build.properties
new file mode 100644
index 00000000..1ff47f92
--- /dev/null
+++ b/positions/build.properties
@@ -0,0 +1,8 @@
+#Created by .winboll/winboll_app_build.gradle
+#Thu Oct 02 13:16:14 GMT 2025
+stageCount=8
+libraryProject=
+baseVersion=15.0
+publishVersion=15.0.7
+buildCount=29
+baseBetaVersion=15.0.8
diff --git a/positions/proguard-rules.pro b/positions/proguard-rules.pro
new file mode 100644
index 00000000..64b4a059
--- /dev/null
+++ b/positions/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/positions/src/beta/AndroidManifest.xml b/positions/src/beta/AndroidManifest.xml
new file mode 100644
index 00000000..d783f522
--- /dev/null
+++ b/positions/src/beta/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/beta/res/values-zh/strings.xml b/positions/src/beta/res/values-zh/strings.xml
new file mode 100644
index 00000000..7732c38a
--- /dev/null
+++ b/positions/src/beta/res/values-zh/strings.xml
@@ -0,0 +1,4 @@
+
+
+ 寻龙记#
+
diff --git a/positions/src/beta/res/values/strings.xml b/positions/src/beta/res/values/strings.xml
new file mode 100644
index 00000000..5dc93b9f
--- /dev/null
+++ b/positions/src/beta/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+ Positions +
+
+
diff --git a/positions/src/main/AndroidManifest.xml b/positions/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d3022109
--- /dev/null
+++ b/positions/src/main/AndroidManifest.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/App.java b/positions/src/main/java/cc/winboll/studio/positions/App.java
new file mode 100644
index 00000000..9ca6fd7e
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/App.java
@@ -0,0 +1,349 @@
+package cc.winboll.studio.positions;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+import cc.winboll.studio.libappbase.GlobalApplication;
+import com.hjq.toast.ToastUtils;
+import com.hjq.toast.style.WhiteToastStyle;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+
+public class App extends GlobalApplication {
+
+ private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ setIsDebuging(BuildConfig.DEBUG);
+
+ WinBoLLActivityManager.init(this);
+
+ // 初始化 Toast 框架
+ ToastUtils.init(this);
+ // 设置 Toast 布局样式
+ //ToastUtils.setView(R.layout.view_toast);
+ ToastUtils.setStyle(new WhiteToastStyle());
+ ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
+
+ //CrashHandler.getInstance().registerGlobal(this);
+ //CrashHandler.getInstance().registerPart(this);
+ }
+
+ public static void write(InputStream input, OutputStream output) throws IOException {
+ byte[] buf = new byte[1024 * 8];
+ int len;
+ while ((len = input.read(buf)) != -1) {
+ output.write(buf, 0, len);
+ }
+ }
+
+ public static void write(File file, byte[] data) throws IOException {
+ File parent = file.getParentFile();
+ if (parent != null && !parent.exists()) parent.mkdirs();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(data);
+ FileOutputStream output = new FileOutputStream(file);
+ try {
+ write(input, output);
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static String toString(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ write(input, output);
+ try {
+ return output.toString("UTF-8");
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static void closeIO(Closeable... closeables) {
+ for (Closeable closeable : closeables) {
+ try {
+ if (closeable != null) closeable.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ public static class CrashHandler {
+
+ public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
+
+ private static CrashHandler sInstance;
+
+ private PartCrashHandler mPartCrashHandler;
+
+ public static CrashHandler getInstance() {
+ if (sInstance == null) {
+ sInstance = new CrashHandler();
+ }
+ return sInstance;
+ }
+
+ public void registerGlobal(Context context) {
+ registerGlobal(context, null);
+ }
+
+ public void registerGlobal(Context context, String crashDir) {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
+ }
+
+ public void unregister() {
+ Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
+ }
+
+ public void registerPart(Context context) {
+ unregisterPart(context);
+ mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
+ MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
+ }
+
+ public void unregisterPart(Context context) {
+ if (mPartCrashHandler != null) {
+ mPartCrashHandler.isRunning.set(false);
+ mPartCrashHandler = null;
+ }
+ }
+
+ private static class PartCrashHandler implements Runnable {
+
+ private final Context mContext;
+
+ public AtomicBoolean isRunning = new AtomicBoolean(true);
+
+ public PartCrashHandler(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ public void run() {
+ while (isRunning.get()) {
+ try {
+ Looper.loop();
+ } catch (final Throwable e) {
+ e.printStackTrace();
+ if (isRunning.get()) {
+ MAIN_HANDLER.post(new Runnable(){
+
+ @Override
+ public void run() {
+ Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
+ }
+ });
+ } else {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException)e;
+ } else {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
+
+ private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
+
+ private final Context mContext;
+
+ private final File mCrashDir;
+
+ public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
+ this.mContext = context;
+ this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ try {
+
+ String log = buildLog(throwable);
+ writeLog(log);
+
+ try {
+ Intent intent = new Intent(mContext, CrashActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Intent.EXTRA_TEXT, log);
+ mContext.startActivity(intent);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ writeLog(e.toString());
+ }
+
+ throwable.printStackTrace();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+
+ } catch (Throwable e) {
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ }
+
+ private String buildLog(Throwable throwable) {
+ String time = DATE_FORMAT.format(new Date());
+
+ String versionName = "unknown";
+ long versionCode = 0;
+ try {
+ PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
+ versionName = packageInfo.versionName;
+ versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
+ } catch (Throwable ignored) {}
+
+ LinkedHashMap head = new LinkedHashMap();
+ head.put("Time Of Crash", time);
+ head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
+ head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
+ head.put("App Version", String.format("%s (%d)", versionName, versionCode));
+ head.put("Kernel", getKernel());
+ head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
+ head.put("Fingerprint", Build.FINGERPRINT);
+
+ StringBuilder builder = new StringBuilder();
+
+ for (String key : head.keySet()) {
+ if (builder.length() != 0) builder.append("\n");
+ builder.append(key);
+ builder.append(" : ");
+ builder.append(head.get(key));
+ }
+
+ builder.append("\n\n");
+ builder.append(Log.getStackTraceString(throwable));
+
+ return builder.toString();
+ }
+
+ private void writeLog(String log) {
+ String time = DATE_FORMAT.format(new Date());
+ File file = new File(mCrashDir, "crash_" + time + ".txt");
+ try {
+ write(file, log.getBytes("UTF-8"));
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static String getKernel() {
+ try {
+ return App.toString(new FileInputStream("/proc/version")).trim();
+ } catch (Throwable e) {
+ return e.getMessage();
+ }
+ }
+ }
+ }
+
+ public static final class CrashActivity extends Activity {
+
+ private String mLog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setTheme(android.R.style.Theme_DeviceDefault);
+ setTitle("App Crash");
+
+ mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+
+ ScrollView contentView = new ScrollView(this);
+ contentView.setFillViewport(true);
+
+ HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
+
+ TextView textView = new TextView(this);
+ int padding = dp2px(16);
+ textView.setPadding(padding, padding, padding, padding);
+ textView.setText(mLog);
+ textView.setTextIsSelectable(true);
+ textView.setTypeface(Typeface.DEFAULT);
+ textView.setLinksClickable(true);
+
+ horizontalScrollView.addView(textView);
+ contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ setContentView(contentView);
+ }
+
+ private void restart() {
+ Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ finish();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+ }
+
+ private static int dp2px(float dpValue) {
+ final float scale = Resources.getSystem().getDisplayMetrics().density;
+ return (int) (dpValue * scale + 0.5f);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, android.R.id.copy, 0, android.R.string.copy)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.copy:
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ restart();
+ }
+ }
+}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java b/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java
new file mode 100644
index 00000000..654f31e7
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java
@@ -0,0 +1,266 @@
+package cc.winboll.studio.positions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.Switch;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.positions.activities.LocationActivity;
+import cc.winboll.studio.positions.activities.WinBoLLActivity;
+import cc.winboll.studio.positions.services.MainService;
+import cc.winboll.studio.positions.utils.AppConfigsUtil;
+
+/**
+ * 主页面:仅负责
+ * 1. 位置服务启动/停止(通过 Switch 开关控制)
+ * 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
+ * 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
+ */
+public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
+ public static final String TAG = "MainActivity";
+ // 权限请求码(建议定义为类常量,避免魔法值)
+ private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
+ private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
+
+ // UI 控件:服务控制开关、顶部工具栏
+ private Switch mServiceSwitch;
+ private Button mManagePositionsButton;
+ private Toolbar mToolbar;
+ // 服务相关:服务实例、绑定状态标记
+ //private DistanceRefreshService mDistanceService;
+ private boolean isServiceBound = false;
+
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ // ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
+// private final ServiceConnection mServiceConn = new ServiceConnection() {
+// /**
+// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
+// */
+// @Override
+// public void onServiceConnected(ComponentName name, IBinder service) {
+// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
+// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
+// mDistanceService = binder.getService();
+// isServiceBound = true;
+// }
+//
+// /**
+// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态
+// */
+// @Override
+// public void onServiceDisconnected(ComponentName name) {
+// mDistanceService = null;
+// isServiceBound = false;
+// }
+// };
+
+ // ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ----------------------
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main); // 关联主页面布局
+
+ // 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
+ initToolbar();
+ // 2. 初始化其他控件
+ initViews();
+ // 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
+ if (!checkLocationPermissions()) {
+ requestLocationPermissions();
+ }
+ // 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
+ //bindDistanceService();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
+// if (isServiceBound) {
+// unbindService(mServiceConn);
+// isServiceBound = false;
+// mDistanceService = null;
+// }
+ }
+
+ // ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
+ /**
+ * 初始化顶部 Toolbar,设置页面标题
+ */
+ private void initToolbar() {
+ mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
+ setSupportActionBar(mToolbar);
+ // 给ActionBar设置标题(先判断非空,避免空指针异常)
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setTitle(getString(R.string.app_name));
+ }
+ }
+
+ /**
+ * 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
+ */
+ private void initViews() {
+ mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
+ mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
+
+ mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
+ mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
+
+ // Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
+ mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ // 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
+ if (isChecked && !checkLocationPermissions()) {
+ requestLocationPermissions();
+ return;
+ }
+
+ // 权限就绪:执行服务启停逻辑
+ if (isChecked) {
+ LogUtils.d(TAG, "设置启动服务");
+ AppConfigsUtil.getInstance(MainActivity.this).setIsEnableMainService(true);
+ // 启动服务(startService确保服务独立运行,不受Activity绑定影响)
+ startService(new Intent(MainActivity.this, MainService.class));
+ } else {
+ LogUtils.d(TAG, "设置关闭服务");
+ AppConfigsUtil.getInstance(MainActivity.this).setIsEnableMainService(false);
+ // 停止服务前先解绑,避免服务被Activity持有
+// if (isServiceBound) {
+// unbindService(mServiceConn);
+// isServiceBound = false;
+// }
+ stopService(new Intent(MainActivity.this, MainService.class));
+ }
+
+ mManagePositionsButton.setEnabled(isChecked);
+ }
+ });
+ }
+
+
+ /**
+ * 绑定服务(仅用于获取服务状态,不启动服务)
+ */
+// private void bindDistanceService() {
+// Intent serviceIntent = new Intent(this, MainService.class);
+// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制)
+// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+// }
+
+ // ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
+ /**
+ * 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
+ * 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
+ */
+ public void onPositions(View view) {
+ //ToastUtils.show("onPositions");
+ // 服务已启动:跳转到位置管理页
+ startActivity(new Intent(MainActivity.this, LocationActivity.class));
+ }
+
+ /**
+ * 跳转至“日志页(LogActivity)”(按钮点击触发,需在布局中设置 android:onClick="onLog")
+ * 无服务状态限制,直接跳转
+ */
+ public void onLog(View view) {
+ WinBoLLActivityManager.getInstance().startLogActivity(this); // 调用LogActivity静态方法跳转(保留原逻辑)
+ }
+
+ // ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ----------------------
+ /**
+ * 检查是否拥有「前台+后台」位置权限(适配Android版本差异)
+ * Java7 特性:显式类型判断、无Lambda、兼容低版本API
+ */
+ private boolean checkLocationPermissions() {
+ // 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量)
+ int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
+ boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED);
+
+ // 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本)
+ boolean hasBackgroundPerm = true;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
+ hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED);
+ }
+
+ // 前台+后台权限均满足,才返回true
+ return hasForegroundPerm && hasBackgroundPerm;
+ }
+
+ private void requestLocationPermissions() {
+ // 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限)
+ String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
+ // 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
+ }
+ } else {
+ // 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // 2.1 检查后台权限是否未授予
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框)
+ requestPermissions(
+ new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
+ REQUEST_BACKGROUND_LOCATION_PERMISSION
+ );
+ }
+ }
+ // 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑
+ // doLocationRelatedLogic();
+ }
+ }
+
+// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑)
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ // 处理前台权限申请结果
+ if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // 前台权限同意:自动尝试申请后台权限(如果是API 29+)
+ requestLocationPermissions();
+ } else {
+ // 前台权限拒绝:提示用户(可选:引导跳转到应用设置页)
+ Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
+ }
+ } else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // 后台权限同意:可执行后台定位逻辑
+ Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
+ } else {
+ // 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启)
+ Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java b/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java
new file mode 100644
index 00000000..be3e5011
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/activities/LocationActivity.java
@@ -0,0 +1,227 @@
+package cc.winboll.studio.positions.activities;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/29 18:22
+ * @Describe 位置列表页面(适配MainService GPS接口+规范服务交互+完善生命周期)
+ */
+import android.os.Bundle;
+import android.os.IBinder;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.widget.Toast;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.positions.adapters.PositionAdapter;
+import cc.winboll.studio.positions.models.PositionModel;
+import cc.winboll.studio.positions.models.PositionTaskModel;
+import cc.winboll.studio.positions.services.MainService;
+import cc.winboll.studio.positions.R;
+import java.util.ArrayList;
+
+/**
+ * Java 7 语法适配:
+ * 1. 服务绑定用匿名内部类实现 ServiceConnection
+ * 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
+ * 3. 所有位置/任务操作通过 MainService 接口执行
+ */
+public class LocationActivity extends Activity {
+ private static final String TAG = "LocationActivity";
+
+ private RecyclerView mRvPosition;
+ private PositionAdapter mPositionAdapter;
+ private ArrayList mLocalPosCache; // 本地位置缓存(与MainService同步)
+
+ // MainService 引用+绑定状态
+ private MainService mMainService;
+ private boolean isServiceBound = false;
+
+ // 服务连接(Java 7 匿名内部类实现)
+ private ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // 假设 MainService 用 LocalBinder 暴露实例(Java 7 强转)
+ MainService.LocalBinder binder = (MainService.LocalBinder) service;
+ mMainService = binder.getService();
+ isServiceBound = true;
+
+ LogUtils.d(TAG, "MainService绑定成功,开始同步数据");
+ // 从MainService同步初始数据(位置+任务)
+ syncDataFromMainService();
+ // 初始化Adapter(传入MainService实例,确保任务数据从服务获取)
+ initPositionAdapter();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ LogUtils.w(TAG, "MainService断开连接,清空引用");
+ mMainService = null;
+ isServiceBound = false;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_location);
+
+ // 初始化视图+本地缓存
+ initView();
+ mLocalPosCache = new ArrayList();
+
+ // 绑定MainService(确保Activity启动时就拿到服务实例)
+ bindMainService();
+ }
+
+ /**
+ * 初始化视图(RecyclerView)
+ */
+ private void initView() {
+ mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
+ // Java 7 显式设置布局管理器(LinearLayoutManager)
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
+ mRvPosition.setLayoutManager(layoutManager);
+ }
+
+ /**
+ * 绑定MainService(Java 7 显式Intent)
+ */
+ private void bindMainService() {
+ Intent serviceIntent = new Intent(this, MainService.class);
+ // 绑定服务(BIND_AUTO_CREATE:服务不存在时自动创建)
+ bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
+ LogUtils.d(TAG, "发起MainService绑定请求");
+ }
+
+ /**
+ * 从MainService同步数据(位置+任务)
+ */
+ private void syncDataFromMainService() {
+ if (!isServiceBound || mMainService == null) {
+ LogUtils.w(TAG, "同步数据失败:MainService未绑定");
+ showToast("服务未就绪,无法加载数据");
+ return;
+ }
+
+ // 同步位置数据(从服务获取最新列表)
+ ArrayList servicePosList = mMainService.getPositionList();
+ if (servicePosList != null && !servicePosList.isEmpty()) {
+ mLocalPosCache.clear();
+ mLocalPosCache.addAll(servicePosList);
+ LogUtils.d(TAG, "从MainService同步位置数据完成:数量=" + mLocalPosCache.size());
+ }
+
+ // 同步任务数据(无需本地缓存,Adapter直接从服务获取)
+ ArrayList serviceTaskList = mMainService.getAllTasks();
+ LogUtils.d(TAG, "从MainService同步任务数据完成:数量=" + serviceTaskList.size());
+ }
+
+ /**
+ * 初始化PositionAdapter(核心:传入MainService实例)
+ */
+ private void initPositionAdapter() {
+ if (mMainService == null) {
+ LogUtils.e(TAG, "初始化Adapter失败:MainService为空");
+ return;
+ }
+
+ // Java 7 显式初始化Adapter,传入上下文+本地位置缓存+MainService实例
+ mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
+
+ // 设置Adapter回调(处理位置删除/保存,最终同步到MainService)
+ mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
+ @Override
+ public void onDeleteClick(int position) {
+ // 删除逻辑:先删本地缓存,再调用MainService接口删服务数据
+ if (position < 0 || position >= mLocalPosCache.size()) {
+ LogUtils.w(TAG, "删除位置失败:无效索引=" + position);
+ return;
+ }
+ PositionModel deletePos = mLocalPosCache.get(position);
+ if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
+ // 1. 调用MainService接口删除服务端数据
+ mMainService.removePosition(deletePos.getPositionId());
+ // 2. 删除本地缓存数据
+ mLocalPosCache.remove(position);
+ // 3. 通知Adapter刷新
+ mPositionAdapter.notifyItemRemoved(position);
+ showToast("删除位置成功:" + deletePos.getMemo());
+ LogUtils.d(TAG, "删除位置完成:ID=" + deletePos.getPositionId() + "(已同步MainService)");
+ }
+ }
+ });
+
+ mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
+ @Override
+ public void onSavePositionClick(int position, PositionModel updatedPos) {
+ // 保存逻辑:先更本地缓存,再调用MainService接口更新服务数据
+ if (!isServiceBound || mMainService == null) {
+ LogUtils.w(TAG, "保存位置失败:MainService未绑定");
+ showToast("服务未就绪,保存失败");
+ return;
+ }
+ if (position < 0 || position >= mLocalPosCache.size()) {
+ LogUtils.w(TAG, "保存位置失败:无效索引=" + position);
+ return;
+ }
+
+ // 1. 调用MainService接口更新服务端数据
+ mMainService.updatePosition(updatedPos);
+ // 2. 更新本地缓存数据
+ mLocalPosCache.set(position, updatedPos);
+ // 3. 通知Adapter刷新(可选,Adapter已本地同步)
+ mPositionAdapter.notifyItemChanged(position);
+ showToast("保存位置成功:" + updatedPos.getMemo());
+ LogUtils.d(TAG, "保存位置完成:ID=" + updatedPos.getPositionId() + "(已同步MainService)");
+ }
+ });
+
+ // 设置Adapter到RecyclerView
+ mRvPosition.setAdapter(mPositionAdapter);
+ LogUtils.d(TAG, "PositionAdapter初始化完成(已绑定MainService)");
+ }
+
+ /**
+ * 显示Toast(Java 7 显式Toast.makeText)
+ */
+ private void showToast(String content) {
+ Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // 1. 释放Adapter资源(反注册服务监听,避免内存泄漏)
+ if (mPositionAdapter != null) {
+ mPositionAdapter.release();
+ }
+
+ // 2. 解绑MainService(避免Activity销毁后服务仍被持有)
+ if (isServiceBound) {
+ unbindService(mServiceConnection);
+ LogUtils.d(TAG, "MainService解绑完成");
+ }
+ }
+
+ public static class LocalBinder extends android.os.Binder {
+ // 持有 MainService 实例引用
+ private MainService mService;
+
+ // 构造时传入服务实例
+ public LocalBinder(MainService service) {
+ this.mService = service;
+ }
+
+ // 对外提供获取服务实例的方法(供Activity调用)
+ public MainService getService() {
+ return mService;
+ }
+ }
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/activities/WinBoLLActivity.java b/positions/src/main/java/cc/winboll/studio/positions/activities/WinBoLLActivity.java
new file mode 100644
index 00000000..3cf13b76
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/activities/WinBoLLActivity.java
@@ -0,0 +1,60 @@
+package cc.winboll.studio.positions.activities;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/29 00:11
+ * @Describe WinBoLL 窗口基础类
+ */
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MenuItem;
+import androidx.appcompat.app.AppCompatActivity;
+import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.libappbase.LogUtils;
+
+public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
+
+ public static final String TAG = "WinBoLLActivity";
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ LogUtils.d(TAG, String.format("onResume %s", getTag()));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ /*if (item.getItemId() == R.id.item_log) {
+ WinBoLLActivityManager.getInstance().startLogActivity(this);
+ return true;
+ } else if (item.getItemId() == R.id.item_home) {
+ startActivity(new Intent(this, MainActivity.class));
+ return true;
+ }*/
+ // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ WinBoLLActivityManager.getInstance().add(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ WinBoLLActivityManager.getInstance().registeRemove(this);
+ }
+}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java b/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java
new file mode 100644
index 00000000..ab60875d
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/adapters/PositionAdapter.java
@@ -0,0 +1,556 @@
+package cc.winboll.studio.positions.adapters;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/29 20:25
+ * @Describe 位置数据适配器(完全独立,无未知接口依赖,仅用LocationActivity缓存数据)
+ */
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+import android.text.TextUtils;
+import androidx.recyclerview.widget.RecyclerView;
+
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.positions.R;
+import cc.winboll.studio.positions.models.PositionModel;
+import cc.winboll.studio.positions.models.PositionTaskModel;
+import cc.winboll.studio.positions.services.MainService;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Java 7 语法适配:
+ * 1. 移除 Lambda/方法引用,用匿名内部类替代
+ * 2. 集合操作使用迭代器(避免 ConcurrentModificationException)
+ * 3. 弱引用管理 MainService,避免内存泄漏
+ * 4. 所有任务数据从 MainService 获取,更新通过 MainService 接口
+ */
+public class PositionAdapter extends RecyclerView.Adapter implements MainService.TaskUpdateListener {
+ public static final String TAG = "PositionAdapter";
+
+ // 视图类型常量(Java 7 静态常量定义)
+ private static final int VIEW_TYPE_SIMPLE = 0;
+ private static final int VIEW_TYPE_EDIT = 1;
+
+ // 默认配置常量(统一管理,避免魔法值)
+ private static final String DEFAULT_MEMO = "无备注";
+ private static final String DEFAULT_TASK_DESC = "新任务";
+ private static final int DEFAULT_TASK_DISTANCE = 50; // 单位:米
+ private static final String DISTANCE_FORMAT = "实时距离:%.1f 米";
+ private static final String DISTANCE_DISABLED = "实时距离:未启用";
+ private static final String DISTANCE_ERROR = "实时距离:计算失败";
+
+ // 核心依赖(Java 7 弱引用+集合定义)
+ private final Context mContext;
+ private final ArrayList mCachedPositionList; // 位置缓存(从Activity传入,最终需与MainService同步)
+ private final WeakReference mMainServiceRef; // 弱引用MainService,避免内存泄漏
+ private final ConcurrentHashMap mPosDistanceViewMap; // 距离控件缓存(优化UI更新)
+
+ // 回调接口(与Activity交互,仅处理位置逻辑,任务逻辑直接调用MainService)
+ public interface OnDeleteClickListener {
+ void onDeleteClick(int position);
+ }
+
+ public interface OnSavePositionClickListener {
+ void onSavePositionClick(int position, PositionModel updatedPos);
+ }
+
+ private OnDeleteClickListener mOnDeleteListener;
+ private OnSavePositionClickListener mOnSavePosListener;
+
+ // =========================================================================
+ // 构造函数(Java 7 风格:初始化依赖+注册任务监听)
+ // =========================================================================
+ public PositionAdapter(Context context, ArrayList cachedPositionList, MainService mainService) {
+ this.mContext = context;
+ // 容错处理:避免传入null导致空指针
+ this.mCachedPositionList = (cachedPositionList != null) ? cachedPositionList : new ArrayList();
+ // 弱引用MainService:防止Adapter持有Service导致内存泄漏(Java 7 弱引用语法)
+ this.mMainServiceRef = new WeakReference(mainService);
+ // 初始化距离控件缓存(线程安全集合,适配多线程更新场景)
+ this.mPosDistanceViewMap = new ConcurrentHashMap();
+
+ // 注册MainService任务监听:服务任务变化时自动刷新Adapter(Java 7 接口实现)
+ if (mainService != null) {
+ mainService.registerTaskUpdateListener(this);
+ LogUtils.d(TAG, "已注册MainService任务监听,确保任务数据与服务同步");
+ } else {
+ LogUtils.w(TAG, "构造函数:MainService为空,任务数据无法同步");
+ }
+
+ LogUtils.d(TAG, "Adapter初始化完成:位置数量=" + mCachedPositionList.size());
+ }
+
+ // =========================================================================
+ // RecyclerView 核心方法(Java 7 语法适配)
+ // =========================================================================
+ @Override
+ public int getItemViewType(int position) {
+ // 从位置缓存获取状态,判断视图类型(简单/编辑)
+ PositionModel posModel = getPositionByIndex(position);
+ return (posModel != null && posModel.isSimpleView()) ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ // 根据视图类型加载对应布局(Java 7 条件判断)
+ if (viewType == VIEW_TYPE_SIMPLE) {
+ View simpleView = inflater.inflate(R.layout.item_position_simple, parent, false);
+ return new SimpleViewHolder(simpleView);
+ } else {
+ View editView = inflater.inflate(R.layout.item_position_edit, parent, false);
+ return new EditViewHolder(editView);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ PositionModel posModel = getPositionByIndex(position);
+ if (posModel == null) {
+ LogUtils.w(TAG, "onBindViewHolder:位置模型为空(索引=" + position + "),跳过绑定");
+ return;
+ }
+ String posId = posModel.getPositionId();
+
+ // 按视图类型绑定数据(Java 7 类型判断)
+ if (holder instanceof SimpleViewHolder) {
+ bindSimpleView((SimpleViewHolder) holder, posModel);
+ } else if (holder instanceof EditViewHolder) {
+ bindEditView((EditViewHolder) holder, posModel, position);
+ }
+
+ // 缓存当前位置的距离控件(后续局部更新距离时直接使用)
+ TextView distanceView = (holder instanceof SimpleViewHolder)
+ ? ((SimpleViewHolder) holder).tvSimpleDistance
+ : ((EditViewHolder) holder).tvEditDistance;
+ if (distanceView != null && !TextUtils.isEmpty(posId)) {
+ mPosDistanceViewMap.put(posId, distanceView);
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
+ super.onViewDetachedFromWindow(holder);
+ // 视图离开屏幕时,移除距离控件缓存(避免内存泄漏+引用失效控件)
+ PositionModel posModel = getPositionByIndex(holder.getAdapterPosition());
+ if (posModel != null && !TextUtils.isEmpty(posModel.getPositionId())) {
+ mPosDistanceViewMap.remove(posModel.getPositionId());
+ LogUtils.d(TAG, "视图脱离屏幕:移除位置ID=" + posModel.getPositionId() + "的距离控件缓存");
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ // 直接从位置缓存获取数量(数据源唯一)
+ return mCachedPositionList.size();
+ }
+
+ // =========================================================================
+ // 视图绑定逻辑(Java 7 风格:任务数据从MainService获取)
+ // =========================================================================
+ /**
+ * 绑定简单视图(仅显示数据,点击切换到编辑视图)
+ */
+ private void bindSimpleView(final SimpleViewHolder holder, final PositionModel posModel) {
+ // 1. 显示经纬度(Java 7 String.format格式化)
+ holder.tvSimpleLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
+ holder.tvSimpleLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
+
+ // 2. 显示备注(无备注时显示默认文本)
+ String memo = posModel.getMemo();
+ holder.tvSimpleMemo.setText("备注:" + (TextUtils.isEmpty(memo) ? DEFAULT_MEMO : memo));
+
+ // 3. 显示实时距离(从位置模型取数,调用工具方法更新显示)
+ updateDistanceDisplay(holder.tvSimpleDistance, posModel);
+
+ // 4. 点击切换到编辑视图(Java 7 匿名内部类实现点击事件)
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ posModel.setIsSimpleView(false); // 修改位置缓存状态
+ // 通知RecyclerView刷新当前项(精准更新,避免全量刷新)
+ notifyItemChanged(getPositionIndexById(posModel.getPositionId()));
+ LogUtils.d(TAG, "简单视图点击:位置ID=" + posModel.getPositionId() + ",切换到编辑视图");
+ }
+ });
+ }
+
+ /**
+ * 绑定编辑视图(支持修改备注、开关距离、删除/保存位置、新增任务)
+ */
+ private void bindEditView(final EditViewHolder holder, final PositionModel posModel, final int position) {
+ final String posId = posModel.getPositionId();
+
+ // 1. 显示经纬度(不可编辑,仅展示)
+ holder.tvEditLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
+ holder.tvEditLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
+
+ // 2. 显示备注(编辑框赋值,光标定位到末尾)
+ String memo = posModel.getMemo();
+ if (!TextUtils.isEmpty(memo)) {
+ holder.etEditMemo.setText(memo);
+ holder.etEditMemo.setSelection(memo.length()); // 光标定位到文本末尾
+ } else {
+ holder.etEditMemo.setText(""); // 无备注时清空编辑框
+ }
+
+ // 3. 显示实时距离(与简单视图逻辑一致)
+ updateDistanceDisplay(holder.tvEditDistance, posModel);
+
+ // 4. 设置距离开关状态(匹配位置缓存中的启用状态)
+ holder.rgDistanceSwitch.check(posModel.isEnableRealPositionDistance()
+ ? R.id.rb_distance_enable
+ : R.id.rb_distance_disable);
+
+ // 5. 取消编辑:切换回简单视图(Java 7 匿名内部类)
+ holder.btnCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ posModel.setIsSimpleView(true);
+ notifyItemChanged(position);
+ hideSoftKeyboard(v); // 隐藏软键盘(提升用户体验)
+ LogUtils.d(TAG, "取消编辑:位置ID=" + posId + ",切换回简单视图");
+ }
+ });
+
+ // 6. 删除位置:回调Activity处理(Adapter不直接删数据,由Activity同步MainService)
+ holder.btnDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnDeleteListener != null) {
+ mOnDeleteListener.onDeleteClick(position); // 通知Activity删除指定索引
+ }
+ hideSoftKeyboard(v);
+ LogUtils.d(TAG, "触发删除:通知Activity处理位置ID=" + posId + "的删除逻辑");
+ }
+ });
+
+ // 7. 保存位置:回调Activity保存(收集参数→构建更新模型→通知Activity)
+ holder.btnSave.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // 收集编辑后的参数(备注+距离启用状态)
+ String newMemo = holder.etEditMemo.getText().toString().trim();
+ boolean isDistanceEnable = (holder.rgDistanceSwitch.getCheckedRadioButtonId() == R.id.rb_distance_enable);
+
+ // 构建更新后的位置模型(保留原核心数据,仅更新可编辑字段)
+ PositionModel updatedPos = new PositionModel();
+ updatedPos.setPositionId(posId); // 保留原ID(不可修改)
+ updatedPos.setLongitude(posModel.getLongitude()); // 保留原经度(不可编辑)
+ updatedPos.setLatitude(posModel.getLatitude()); // 保留原纬度(不可编辑)
+ updatedPos.setMemo(newMemo); // 更新备注(用户编辑)
+ updatedPos.setIsEnableRealPositionDistance(isDistanceEnable); // 更新距离状态
+ updatedPos.setIsSimpleView(true); // 切换回简单视图
+
+ // 回调Activity保存(由Activity同步MainService+位置缓存,Adapter不处理逻辑)
+ if (mOnSavePosListener != null) {
+ mOnSavePosListener.onSavePositionClick(position, updatedPos);
+ }
+
+ // 本地同步状态(避免刷新延迟,直接修改位置缓存)
+ posModel.setMemo(newMemo);
+ posModel.setIsEnableRealPositionDistance(isDistanceEnable);
+ posModel.setIsSimpleView(true);
+ notifyItemChanged(position); // 刷新当前项,显示更新后的状态
+ hideSoftKeyboard(v);
+ LogUtils.d(TAG, "触发保存:位置ID=" + posId + ",新备注=" + newMemo + ",距离启用=" + isDistanceEnable);
+ }
+ });
+
+ // 8. 绑定任务视图(显示任务数量+新增任务,数据从MainService获取)
+ bindTaskView(holder, posId);
+ }
+
+ /**
+ * 绑定任务视图(编辑模式专属:从MainService获取任务数据,新增任务调用服务接口)
+ */
+ private void bindTaskView(final EditViewHolder holder, final String posId) {
+ // 1. 从MainService获取当前位置的任务数量(Java 7 迭代器遍历服务数据)
+ int taskCount = 0;
+ MainService mainService = mMainServiceRef.get();
+ if (mainService != null) {
+ ArrayList posTasks = mainService.getTasksByPositionId(posId);
+ taskCount = (posTasks != null) ? posTasks.size() : 0;
+ }
+ // 显示任务数量(简化设计,实际可扩展为任务列表)
+ holder.tvTaskCount.setText("任务数量:" + taskCount);
+
+ // 2. 新增任务:调用MainService接口(不操作本地缓存,数据直接写入服务)
+ holder.btnAddTask.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ MainService mainService = mMainServiceRef.get();
+ if (mainService == null) {
+ LogUtils.e(TAG, "新增任务失败:MainService已回收(弱引用失效)");
+ return;
+ }
+
+ // 构建默认任务模型(Java 7 显式初始化)
+ PositionTaskModel newTask = new PositionTaskModel();
+ newTask.setTaskId(PositionTaskModel.genTaskId()); // 生成唯一任务ID(需在PositionTaskModel实现静态方法)
+ newTask.setPositionId(posId); // 绑定当前位置ID
+ newTask.setTaskDescription(DEFAULT_TASK_DESC); // 默认任务描述
+ newTask.setIsEnable(true); // 默认启用任务
+ newTask.setDiscussDistance(DEFAULT_TASK_DISTANCE);// 默认任务距离(50米)
+
+ // 调用MainService接口新增任务(数据写入服务,由服务处理持久化+通知刷新)
+ mainService.addPositionTask(newTask);
+ hideSoftKeyboard(v);
+ LogUtils.d(TAG, "触发新增任务:调用MainService接口,位置ID=" + posId + ",任务ID=" + newTask.getTaskId());
+ }
+ });
+ }
+
+ // =========================================================================
+ // 工具方法(Java 7 风格:无Lambda,纯匿名内部类+迭代器)
+ // =========================================================================
+ /**
+ * 更新距离显示(根据位置模型状态,显示不同文本+颜色)
+ */
+ private void updateDistanceDisplay(TextView distanceView, PositionModel posModel) {
+ if (distanceView == null || posModel == null) {
+ LogUtils.w(TAG, "updateDistanceDisplay:参数为空(控件/位置模型)");
+ return;
+ }
+
+ // 场景1:距离未启用
+ if (!posModel.isEnableRealPositionDistance()) {
+ distanceView.setText(DISTANCE_DISABLED);
+ distanceView.setTextColor(mContext.getResources().getColor(R.color.gray));
+ return;
+ }
+
+ // 场景2:距离计算失败(用-1标记失败状态)
+ double distance = posModel.getRealPositionDistance();
+ if (distance < 0) {
+ distanceView.setText(DISTANCE_ERROR);
+ distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
+ return;
+ }
+
+ // 场景3:正常显示距离(按距离范围设置颜色,提升视觉区分度)
+ distanceView.setText(String.format(DISTANCE_FORMAT, distance));
+ if (distance <= 100) {
+ distanceView.setTextColor(mContext.getResources().getColor(R.color.green)); // 近距离(≤100米)
+ } else if (distance <= 500) {
+ distanceView.setTextColor(mContext.getResources().getColor(R.color.yellow));// 中距离(≤500米)
+ } else {
+ distanceView.setTextColor(mContext.getResources().getColor(R.color.red)); // 远距离(>500米)
+ }
+ }
+
+ /**
+ * 根据索引获取位置模型(从位置缓存取数,容错处理)
+ */
+ private PositionModel getPositionByIndex(int index) {
+ if (mCachedPositionList == null || index < 0 || index >= mCachedPositionList.size()) {
+ LogUtils.w(TAG, "getPositionByIndex:无效索引(" + index + ")或位置缓存为空");
+ return null;
+ }
+ return mCachedPositionList.get(index);
+ }
+
+ /**
+ * 根据位置ID获取列表索引(用于精准刷新视图)
+ */
+ private int getPositionIndexById(String positionId) {
+ if (TextUtils.isEmpty(positionId) || mCachedPositionList == null || mCachedPositionList.isEmpty()) {
+ LogUtils.w(TAG, "getPositionIndexById:参数无效(位置ID/缓存为空)");
+ return -1;
+ }
+
+ // Java 7 增强for循环遍历(替代Lambda,适配Java 7语法)
+ for (int i = 0; i < mCachedPositionList.size(); i++) {
+ PositionModel pos = mCachedPositionList.get(i);
+ if (positionId.equals(pos.getPositionId())) {
+ return i; // 找到匹配ID,返回索引
+ }
+ }
+ LogUtils.w(TAG, "getPositionIndexById:未找到位置ID=" + positionId);
+ return -1;
+ }
+
+ /**
+ * 局部更新距离UI(仅更新指定位置的距离,避免全量刷新卡顿)
+ */
+ public void updateSinglePositionDistance(String positionId) {
+ // 校验参数:位置ID无效或控件未缓存,直接返回
+ if (TextUtils.isEmpty(positionId) || !mPosDistanceViewMap.containsKey(positionId)) {
+ LogUtils.w(TAG, "updateSinglePositionDistance:位置ID无效或控件未缓存(ID=" + positionId + ")");
+ return;
+ }
+
+ // 从MainService获取最新位置模型(确保距离值是服务端最新)
+ PositionModel latestPos = null;
+ MainService mainService = mMainServiceRef.get();
+ if (mainService != null) {
+ ArrayList servicePosList = mainService.getPositionList();
+ if (servicePosList != null && !servicePosList.isEmpty()) {
+ // Java 7 迭代器遍历服务端位置列表,找到目标位置
+ Iterator posIter = servicePosList.iterator();
+ while (posIter.hasNext()) {
+ PositionModel pos = posIter.next();
+ if (positionId.equals(pos.getPositionId())) {
+ latestPos = pos;
+ break;
+ }
+ }
+ }
+ }
+
+ // 用服务端最新距离更新UI(直接操作缓存的距离控件,无需刷新整个项)
+ if (latestPos != null) {
+ TextView distanceView = mPosDistanceViewMap.get(positionId);
+ updateDistanceDisplay(distanceView, latestPos);
+ LogUtils.d(TAG, "局部更新距离完成:位置ID=" + positionId + ",最新距离=" + latestPos.getRealPositionDistance() + "米");
+ } else {
+ LogUtils.w(TAG, "局部更新距离失败:未在MainService找到位置ID=" + positionId);
+ }
+ }
+
+ /**
+ * 全量更新位置数据(从MainService同步最新位置列表,刷新UI)
+ */
+ public void updateAllPositionData(ArrayList newPosList) {
+ if (newPosList == null) {
+ LogUtils.w(TAG, "updateAllPositionData:新位置列表为空,跳过更新");
+ return;
+ }
+
+ // 同步服务端最新位置数据到本地缓存
+ this.mCachedPositionList.clear();
+ this.mCachedPositionList.addAll(newPosList);
+ // 清空旧距离控件缓存(避免引用失效控件)
+ mPosDistanceViewMap.clear();
+ // 通知RecyclerView全量刷新UI
+ notifyDataSetChanged();
+ LogUtils.d(TAG, "全量更新位置数据完成:当前位置数量=" + mCachedPositionList.size() + "(数据来源:MainService)");
+ }
+
+ /**
+ * 隐藏软键盘(编辑完成后调用,提升用户体验)
+ */
+ private void hideSoftKeyboard(View view) {
+ if (mContext == null || view == null) {
+ LogUtils.w(TAG, "hideSoftKeyboard:参数为空(上下文/视图),无法隐藏键盘");
+ return;
+ }
+
+ // Java 7 显式获取输入法服务,避免Lambda
+ InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0); // 强制隐藏软键盘
+ }
+ }
+
+ // =========================================================================
+ // 实现 MainService.TaskUpdateListener 接口(服务任务变化时回调)
+ // =========================================================================
+ @Override
+ public void onTaskUpdated() {
+ LogUtils.d(TAG, "收到MainService任务更新通知(任务新增/删除/状态变化),刷新UI");
+ // 任务数据变化时,全量刷新Adapter(确保任务数量等显示同步)
+ notifyDataSetChanged();
+ }
+
+ // =========================================================================
+ // 回调设置方法(供LocationActivity调用,绑定交互逻辑)
+ // =========================================================================
+ public void setOnDeleteClickListener(OnDeleteClickListener listener) {
+ this.mOnDeleteListener = listener;
+ }
+
+ public void setOnSavePositionClickListener(OnSavePositionClickListener listener) {
+ this.mOnSavePosListener = listener;
+ }
+
+ // =========================================================================
+ // 资源释放(Activity销毁时调用,避免内存泄漏)
+ // =========================================================================
+ public void release() {
+ // 1. 反注册MainService任务监听(解除与服务的绑定,避免内存泄漏)
+ MainService mainService = mMainServiceRef.get();
+ if (mainService != null) {
+ mainService.unregisterTaskUpdateListener(this);
+ LogUtils.d(TAG, "已反注册MainService任务监听,避免内存泄漏");
+ }
+
+ // 2. 清空本地缓存(解除控件/数据引用,帮助GC回收)
+ mPosDistanceViewMap.clear();
+ if (mCachedPositionList != null) {
+ mCachedPositionList.clear();
+ }
+
+ // 3. 置空回调实例(避免持有Activity引用导致内存泄漏)
+ mOnDeleteListener = null;
+ mOnSavePosListener = null;
+
+ LogUtils.d(TAG, "Adapter资源已完全释放(缓存清空+监听反注册)");
+ }
+
+ // =========================================================================
+ // 静态内部类:视图Holder(Java 7 静态内部类,不持有外部引用,避免内存泄漏)
+ // =========================================================================
+ /**
+ * 简单视图Holder(仅显示数据,对应布局:item_position_simple.xml)
+ */
+ public static class SimpleViewHolder extends RecyclerView.ViewHolder {
+ TextView tvSimpleLon; // 经度显示控件
+ TextView tvSimpleLat; // 纬度显示控件
+ TextView tvSimpleMemo; // 备注显示控件
+ TextView tvSimpleDistance;// 实时距离显示控件
+
+ public SimpleViewHolder(View itemView) {
+ super(itemView);
+ // 绑定布局控件(与XML中ID严格对应,避免运行时空指针)
+ tvSimpleLon = (TextView) itemView.findViewById(R.id.tv_simple_longitude);
+ tvSimpleLat = (TextView) itemView.findViewById(R.id.tv_simple_latitude);
+ tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo);
+ tvSimpleDistance = (TextView) itemView.findViewById(R.id.tv_simple_distance);
+ }
+ }
+
+ /**
+ * 编辑视图Holder(含编辑控件+功能按钮,对应布局:item_position_edit.xml)
+ */
+ public static class EditViewHolder extends RecyclerView.ViewHolder {
+ TextView tvEditLon; // 经度显示控件(不可编辑)
+ TextView tvEditLat; // 纬度显示控件(不可编辑)
+ EditText etEditMemo; // 备注编辑控件
+ TextView tvEditDistance; // 实时距离显示控件
+ RadioGroup rgDistanceSwitch; // 距离启用/禁用开关组
+ Button btnCancel; // 取消编辑按钮
+ Button btnDelete; // 删除位置按钮
+ Button btnSave; // 保存位置按钮
+ Button btnAddTask; // 新增任务按钮
+ TextView tvTaskCount; // 任务数量显示控件
+
+ public EditViewHolder(View itemView) {
+ super(itemView);
+ // 绑定布局控件(与XML中ID严格对应,避免运行时空指针)
+ tvEditLon = (TextView) itemView.findViewById(R.id.tv_edit_longitude);
+ tvEditLat = (TextView) itemView.findViewById(R.id.tv_edit_latitude);
+ etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo);
+ tvEditDistance = (TextView) itemView.findViewById(R.id.tv_edit_distance);
+ rgDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_distance_switch);
+ btnCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel);
+ btnDelete = (Button) itemView.findViewById(R.id.btn_edit_delete);
+ btnSave = (Button) itemView.findViewById(R.id.btn_edit_save);
+ btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task);
+ tvTaskCount = (TextView) itemView.findViewById(R.id.tv_task_count);
+ }
+ }
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/models/AppConfigsModel.java b/positions/src/main/java/cc/winboll/studio/positions/models/AppConfigsModel.java
new file mode 100644
index 00000000..feb0aa06
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/models/AppConfigsModel.java
@@ -0,0 +1,75 @@
+package cc.winboll.studio.positions.models;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/10/01 04:50
+ * @Describe AppConfigsModel
+ */
+ import cc.winboll.studio.libappbase.BaseBean;
+import android.util.JsonWriter;
+import android.util.JsonReader;
+import java.io.IOException;
+
+public class AppConfigsModel extends BaseBean {
+
+ public static final String TAG = "AppConfigsModel";
+
+ boolean isEnableMainService;
+
+ public AppConfigsModel(boolean isEnableMainService) {
+ this.isEnableMainService = isEnableMainService;
+ }
+
+ public AppConfigsModel() {
+ this.isEnableMainService = false;
+ }
+
+ public void setIsEnableMainService(boolean isEnableMainService) {
+ this.isEnableMainService = isEnableMainService;
+ }
+
+ public boolean isEnableMainService() {
+ return isEnableMainService;
+ }
+
+ @Override
+ public String getName() {
+ return AppConfigsModel.class.getName();
+ }
+
+ // JSON序列化(保存位置数据)
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
+ }
+
+ // JSON反序列化(加载位置数据,校验字段)
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
+ } else {
+ if (name.equals("isEnableDistanceRefreshService")) {
+ setIsEnableMainService(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 从JSON读取位置数据
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue(); // 跳过未知字段
+ }
+ }
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java b/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java
new file mode 100644
index 00000000..7c7c9ca2
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/models/PositionModel.java
@@ -0,0 +1,219 @@
+package cc.winboll.studio.positions.models;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/29 18:57
+ * @Describe 位置数据模型
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.BaseBean;
+import java.io.IOException;
+import java.util.UUID;
+
+public class PositionModel extends BaseBean {
+
+ public static final String TAG = "PositionModel";
+ // 位置唯一标识符(与任务的positionId绑定)
+ String positionId;
+ // 经度(范围:-180~180)
+ double longitude;
+ // 纬度(范围:-90~90)
+ double latitude;
+ // 位置备注(空值时显示“无备注”)
+ String memo;
+ // 定位点与指定点实时距离长度
+ double realPositionDistance;
+ // 是否启用实时距离计算
+ boolean isEnableRealPositionDistance;
+ // 是否显示简单视图(true=简单视图,false=编辑视图)
+ boolean isSimpleView = true;
+
+ // 带参构造(强制初始化位置ID和经纬度)
+ public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) {
+ this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
+ this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制
+ this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制
+ this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
+ this.isEnableRealPositionDistance = isEnableRealPositionDistance;
+ }
+
+ // 无参构造(默认值初始化,避免空指针)
+ public PositionModel() {
+ this.positionId = genPositionId();
+ this.longitude = 0.0;
+ this.latitude = 0.0;
+ this.memo = "无备注";
+ this.isEnableRealPositionDistance = false;
+ }
+
+ public void setRealPositionDistance(double realPositionDistance) {
+ this.realPositionDistance = realPositionDistance;
+ }
+
+ public double getRealPositionDistance() {
+ return realPositionDistance;
+ }
+
+ // ---------------------- Getter/Setter(确保字段有效性) ----------------------
+ public void setPositionId(String positionId) {
+ this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
+ }
+
+ public String getPositionId() {
+ return positionId;
+ }
+
+ public void setIsEnableRealPositionDistance(boolean isEnableRealPositionDistance) {
+ this.isEnableRealPositionDistance = isEnableRealPositionDistance;
+ }
+
+ public boolean isEnableRealPositionDistance() {
+ return isEnableRealPositionDistance;
+ }
+
+ public void setIsSimpleView(boolean isSimpleView) {
+ this.isSimpleView = isSimpleView;
+ }
+
+ public boolean isSimpleView() {
+ return isSimpleView;
+ }
+
+ public void setMemo(String memo) {
+ this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
+ }
+
+ public String getMemo() {
+ return memo;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围
+ }
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ // ---------------------- 父类方法重写 ----------------------
+ @Override
+ public String getName() {
+ return PositionModel.class.getName();
+ }
+
+ // 生成唯一位置ID(与任务ID格式一致,确保关联匹配)
+ public static String genPositionId() {
+ UUID uniqueUuid = UUID.randomUUID();
+ return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一)
+ }
+
+ // JSON序列化(保存位置数据)
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("positionId").value(getPositionId());
+ jsonWriter.name("longitude").value(getLongitude());
+ jsonWriter.name("latitude").value(getLatitude());
+ jsonWriter.name("memo").value(getMemo());
+ jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance());
+ }
+
+ // JSON反序列化(加载位置数据,校验字段)
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
+ } else {
+ if (name.equals("positionId")) {
+ setPositionId(jsonReader.nextString());
+ } else if (name.equals("longitude")) {
+ setLongitude(jsonReader.nextDouble());
+ } else if (name.equals("latitude")) {
+ setLatitude(jsonReader.nextDouble());
+ } else if (name.equals("memo")) {
+ setMemo(jsonReader.nextString());
+ } else if (name.equals("isEnableRealPositionDistance")) {
+ setIsEnableRealPositionDistance(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 从JSON读取位置数据
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue(); // 跳过未知字段
+ }
+ }
+ jsonReader.endObject();
+ return this;
+ }
+
+ // ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ----------------------
+ /**
+ * 计算两个位置之间的直线距离(地球表面最短距离)
+ * @param position1 第一个位置(非null)
+ * @param position2 第二个位置(非null)
+ * @param isKilometer 是否返回千米单位:true→千米,false→米
+ * @return 距离(保留1位小数,符合显示需求)
+ * @throws IllegalArgumentException 位置为null或经纬度无效时抛出
+ */
+ public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) {
+ // 1. 校验参数有效性(避免计算异常)
+ if (position1 == null || position2 == null) {
+ throw new IllegalArgumentException("位置对象不能为null");
+ }
+ double lon1 = position1.getLongitude();
+ double lat1 = position1.getLatitude();
+ double lon2 = position2.getLongitude();
+ double lat2 = position2.getLatitude();
+ // 经纬度范围二次校验(确保有效)
+ if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90
+ || lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {
+ throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180)");
+ }
+
+ // 2. Haversine公式计算(地球半径取6371km,行业标准)
+ final double EARTH_RADIUS_KM = 6371;
+ double radLat1 = Math.toRadians(lat1); // 角度转弧度
+ double radLat2 = Math.toRadians(lat2);
+ double radLon1 = Math.toRadians(lon1);
+ double radLon2 = Math.toRadians(lon2);
+
+ double deltaLat = radLat2 - radLat1; // 纬度差
+ double deltaLon = radLon2 - radLon1; // 经度差
+
+ // 核心公式
+ double a = Math.pow(Math.sin(deltaLat / 2), 2)
+ + Math.cos(radLat1) * Math.cos(radLat2)
+ * Math.pow(Math.sin(deltaLon / 2), 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米)
+
+ // 3. 单位转换+精度处理(保留1位小数,符合显示需求)
+ double distance;
+ if (isKilometer) {
+ distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数
+ } else {
+ distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数
+ }
+
+ return distance;
+ }
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java b/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java
new file mode 100644
index 00000000..722c33da
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/models/PositionTaskModel.java
@@ -0,0 +1,192 @@
+package cc.winboll.studio.positions.models;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/30 02:48
+ * @Describe 位置任务数据模型
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.BaseBean;
+import java.io.IOException;
+import java.util.UUID;
+
+public class PositionTaskModel extends BaseBean {
+
+ public static final String TAG = "PositionTaskModel";
+ // 任务标识符(唯一)
+ String taskId;
+ // 绑定的位置标识符(与PositionModel的positionId一一对应)
+ String positionId;
+ // 任务描述
+ String taskDescription;
+ // 任务距离条件:是否大于设定距离
+ boolean isGreaterThan;
+ // 任务距离条件:是否小于设定距离(与isGreaterThan互斥)
+ boolean isLessThan;
+ // 任务条件距离(单位:米)
+ int discussDistance;
+ // 任务是否已触发
+ boolean isBingo = false;
+ // 是否启用任务
+ boolean isEnable;
+
+ // 带参构造(强制传入positionId,确保任务与位置绑定)
+ public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, boolean isEnable) {
+ this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成
+ this.positionId = positionId; // 强制绑定位置ID
+ this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
+ this.isGreaterThan = isGreaterThan;
+ this.isLessThan = !isGreaterThan; // 确保互斥
+ this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值
+ this.isEnable = isEnable;
+ }
+
+ // 无参构造(初始化默认值,positionId需后续设置)
+ public PositionTaskModel() {
+ this.taskId = genTaskId();
+ this.positionId = "";
+ this.taskDescription = "新任务";
+ this.isGreaterThan = true;
+ this.isLessThan = false; // 初始互斥
+ this.discussDistance = 100; // 默认100米
+ this.isEnable = true;
+ }
+
+ public void setIsBingo(boolean isBingo) {
+ this.isBingo = isBingo;
+ }
+
+ public boolean isBingo() {
+ return isBingo;
+ }
+
+ // ---------------------- Getter/Setter(确保positionId不为空,距离有效) ----------------------
+ public void setTaskId(String taskId) {
+ this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public void setPositionId(String positionId) {
+ this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护
+ }
+
+ public String getPositionId() {
+ return positionId;
+ }
+
+ public void setTaskDescription(String taskDescription) {
+ this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
+ }
+
+ public String getTaskDescription() {
+ return taskDescription;
+ }
+
+ // 修复:确保isGreaterThan和isLessThan互斥
+ public void setIsGreaterThan(boolean isGreaterThan) {
+ this.isGreaterThan = isGreaterThan;
+ this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于
+ }
+
+ public boolean isGreaterThan() {
+ return isGreaterThan;
+ }
+
+ // 修复:确保isLessThan和isGreaterThan互斥
+ public void setIsLessThan(boolean isLessThan) {
+ this.isLessThan = isLessThan;
+ this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于
+ }
+
+ public boolean isLessThan() {
+ return isLessThan;
+ }
+
+ public void setDiscussDistance(int discussDistance) {
+ this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数
+ }
+
+ public int getDiscussDistance() {
+ return discussDistance;
+ }
+
+ public void setIsEnable(boolean isEnable) {
+ this.isEnable = isEnable;
+ }
+
+ public boolean isEnable() {
+ return isEnable;
+ }
+
+ // ---------------------- 父类方法重写 ----------------------
+ @Override
+ public String getName() {
+ return PositionTaskModel.class.getName();
+ }
+
+ // 生成唯一任务ID(与PositionModel保持一致格式)
+ public static String genTaskId() {
+ UUID uniqueUuid = UUID.randomUUID();
+ return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一)
+ }
+
+ // JSON序列化(保存任务数据,包含所有字段)
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("taskId").value(getTaskId());
+ jsonWriter.name("positionId").value(getPositionId());
+ jsonWriter.name("taskDescription").value(getTaskDescription());
+ jsonWriter.name("isGreaterThan").value(isGreaterThan());
+ jsonWriter.name("isLessThan").value(isLessThan());
+ jsonWriter.name("discussDistance").value(getDiscussDistance());
+ jsonWriter.name("isEnable").value(isEnable());
+ }
+
+ // JSON反序列化(加载任务数据,校验字段有效性)
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
+ } else {
+ if (name.equals("taskId")) {
+ setTaskId(jsonReader.nextString());
+ } else if (name.equals("positionId")) {
+ setPositionId(jsonReader.nextString());
+ } else if (name.equals("taskDescription")) {
+ setTaskDescription(jsonReader.nextString());
+ } else if (name.equals("isGreaterThan")) {
+ setIsGreaterThan(jsonReader.nextBoolean());
+ } else if (name.equals("isLessThan")) {
+ setIsLessThan(jsonReader.nextBoolean());
+ } else if (name.equals("discussDistance")) {
+ setDiscussDistance(jsonReader.nextInt());
+ } else if (name.equals("isEnable")) {
+ setIsEnable(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 从JSON读取任务数据(确保反序列化完整)
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue(); // 跳过未知字段,避免崩溃
+ }
+ }
+ jsonReader.endObject();
+ return this;
+ }
+
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/services/AssistantService.java b/positions/src/main/java/cc/winboll/studio/positions/services/AssistantService.java
new file mode 100644
index 00000000..93c18f4b
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/services/AssistantService.java
@@ -0,0 +1,95 @@
+package cc.winboll.studio.positions.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2024/07/19 14:30:57
+ * @Describe 应用主要服务组件类守护进程服务组件类
+ */
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import cc.winboll.studio.positions.services.MainService;
+import cc.winboll.studio.positions.utils.AppConfigsUtil;
+import cc.winboll.studio.positions.utils.ServiceUtil;
+
+public class AssistantService extends Service {
+
+ public final static String TAG = "AssistantService";
+
+ MyServiceConnection mMyServiceConnection;
+ volatile boolean mIsServiceRunning;
+ AppConfigsUtil mAppConfigsUtil;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mAppConfigsUtil = AppConfigsUtil.getInstance(this);
+ if (mMyServiceConnection == null) {
+ mMyServiceConnection = new MyServiceConnection();
+ }
+ // 设置运行参数
+ mIsServiceRunning = false;
+ run();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ run();
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mIsServiceRunning = false;
+ super.onDestroy();
+ }
+
+ //
+ // 运行服务内容
+ //
+ void run() {
+ if (mAppConfigsUtil.isEnableMainService(true)) {
+ if (mIsServiceRunning == false) {
+ // 设置运行状态
+ mIsServiceRunning = true;
+ // 唤醒和绑定主进程
+ wakeupAndBindMain();
+ }
+ }
+ }
+
+ //
+ // 唤醒和绑定主进程
+ //
+ void wakeupAndBindMain() {
+ if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) {
+ startForegroundService(new Intent(AssistantService.this, MainService.class));
+ }
+
+ bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
+ }
+
+ //
+ // 主进程与守护进程连接时需要用到此类
+ //
+ class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (mAppConfigsUtil.isEnableMainService(true)) {
+ wakeupAndBindMain();
+ }
+ }
+ }
+}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/services/DistanceRefreshService.java b/positions/src/main/java/cc/winboll/studio/positions/services/DistanceRefreshService.java
new file mode 100644
index 00000000..0e48a342
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/services/DistanceRefreshService.java
@@ -0,0 +1,433 @@
+//package cc.winboll.studio.positions.services;
+//
+///**
+// * @Author ZhanGSKen&豆包大模型
+// * @Date 2025/09/30 19:53
+// * @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)+ GPS信号加载
+// */
+//import android.app.Service;
+//import android.content.Context;
+//import android.content.Intent;
+//import android.content.pm.PackageManager;
+//import android.location.Location;
+//import android.location.LocationListener;
+//import android.location.LocationManager;
+//import android.os.Binder;
+//import android.os.Build;
+//import android.os.Bundle;
+//import android.os.IBinder;
+//import android.os.Looper;
+//import android.widget.Toast;
+//import cc.winboll.studio.libappbase.LogUtils;
+//import cc.winboll.studio.positions.adapters.PositionAdapter;
+//import cc.winboll.studio.positions.models.AppConfigsModel;
+//import cc.winboll.studio.positions.models.PositionModel;
+//import cc.winboll.studio.positions.models.PositionTaskModel;
+//import cc.winboll.studio.positions.utils.NotificationUtil;
+//import java.util.ArrayList;
+//import java.util.HashSet;
+//import java.util.Iterator;
+//import java.util.Set;
+//import java.util.concurrent.Executors;
+//import java.util.concurrent.ScheduledExecutorService;
+//import java.util.concurrent.TimeUnit;
+//
+///**
+// * 核心职责:
+// * 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
+// * 2. 单例式管理位置/任务数据,提供安全增删改查接口
+// * 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
+// * 4. 内置GPS信号加载(通过LocationManager实时获取位置,解决“等待GPS信号”问题)
+// * 5. 服务启动时启动前台通知(保活后台GPS功能,符合系统规范)
+// * 6. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
+// */
+//public class DistanceRefreshService extends Service {
+// public static final String TAG = "DistanceRefreshService";
+//
+//
+// // 服务状态与配置
+// private boolean isServiceRunning = false;
+//
+// private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
+// // 前台通知相关:记录是否已启动前台服务(避免重复调用startForeground)
+// private boolean isForegroundServiceStarted = false;
+//
+//
+//
+// // 服务绑定与UI回调
+// private final IBinder mBinder = new DistanceBinder();
+//
+//
+//
+//
+// /**
+// * 在主线程显示Toast(避免子线程无法显示Toast的问题)
+// */
+// private void showToastOnMainThread(final String message) {
+// if (Looper.myLooper() == Looper.getMainLooper()) {
+// Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+// } else {
+// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
+// @Override
+// public void run() {
+// Toast.makeText(DistanceRefreshService.this, message, Toast.LENGTH_SHORT).show();
+// }
+// });
+// }
+// }
+//
+// // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
+// public class DistanceBinder extends Binder {
+// /**
+// * 外部绑定后获取服务实例(安全暴露服务引用)
+// */
+// public DistanceRefreshService getService() {
+// return DistanceRefreshService.this;
+// }
+// }
+//
+//
+//
+// @Override
+// public void onCreate() {
+// super.onCreate();
+//
+// // 初始化GPS管理器(提前获取系统服务,避免启动时延迟)
+// mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+// LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
+// run();
+// }
+//
+// @Override
+// public int onStartCommand(Intent intent, int flags, int startId) {
+// run();
+// AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
+// boolean isEnableService = (bean == null) ? false : bean.isEnableMainService();
+// // 服务启用时返回START_STICKY(被杀死后尝试重启),禁用时返回默认值
+// return isEnableService ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
+// }
+//
+// public void run() {
+// // 仅服务未运行时启动(避免重复启动)
+// if (!isServiceRunning) {
+// isServiceRunning = true;
+//
+//
+//
+// startDistanceRefreshTask(); // 启动定时距离计算
+// startForegroundNotification(); // 启动前台通知
+//
+// LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒,前台通知+GPS已启动");
+// } else {
+// LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动(前台通知:" + (isForegroundServiceStarted ? "已启动" : "未启动") + " | GPS:" + (isGpsEnabled ? "已开启" : "未开启") + ")");
+// // 异常场景恢复:补全未启动的组件
+// if (!isForegroundServiceStarted) {
+// startForegroundNotification();
+// LogUtils.d(TAG, "服务 run:前台通知未启动,已恢复");
+// }
+// if (isServiceRunning && !isGpsEnabled) {
+// startGpsLocation();
+// LogUtils.d(TAG, "服务 run:GPS未启动,已恢复");
+// }
+// }
+// }
+//
+// @Override
+// public IBinder onBind(Intent intent) {
+// return null; // 按你的业务逻辑返回,无绑定需求则保留null
+// //LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + " | GPS状态:" + (isGpsEnabled ? "可用" : "不可用") + ")");
+// //return mBinder; // 返回Binder实例,供外部获取服务
+// }
+//
+// /*@Override
+// public boolean onUnbind(Intent intent) {
+// LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
+// // 解绑后清理资源,避免内存泄漏
+// mDistanceReceiver = null;
+// mVisiblePositionIds.clear();
+// // 解绑时不停止GPS(服务仍在后台运行,需持续获取位置)
+// return super.onUnbind(intent);
+// }*/
+//
+// @Override
+// public void onDestroy() {
+// super.onDestroy();
+//
+// LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放(GPS+前台通知+线程池)");
+// }
+//
+// // ---------------------- 前台服务通知管理(与GPS状态联动优化) ----------------------
+// /**
+// * 启动前台服务通知(调用NotificationUtils创建通知,确保仅启动一次)
+// */
+// private void startForegroundNotification() {
+// // 1. 校验:避免重复调用startForeground(系统不允许重复启动)
+// if (isForegroundServiceStarted) {
+// LogUtils.w(TAG, "startForegroundNotification:前台通知已启动,无需重复执行");
+// return;
+// }
+//
+// try {
+//// 2. 初始化通知状态文本(根据GPS初始状态动态显示,避免固定“等待”)
+// String initialStatus;
+// if (isGpsPermissionGranted && isGpsEnabled) {
+// initialStatus = "GPS已就绪,正在获取位置(刷新间隔" + REFRESH_INTERVAL + "秒)";
+// } else if (!isGpsPermissionGranted) {
+// initialStatus = "缺少定位权限,无法获取GPS位置";
+// } else {
+// initialStatus = "GPS未开启,请在设置中打开";
+// }
+//
+//
+//// 5. 标记前台服务已启动
+// isForegroundServiceStarted = true;
+// LogUtils.d(TAG, "startForegroundNotification:前台服务通知启动成功,初始状态:" + initialStatus);
+//
+// } catch (Exception e) {
+//// 捕获异常(如上下文失效、通知渠道未创建)
+// isForegroundServiceStarted = false;
+// LogUtils.d(TAG, "startForegroundNotification:前台通知启动失败" + e);
+// }
+// }
+//
+//
+//
+// /**
+//
+// - 主线程回调Adapter更新UI(避免跨线程操作UI异常)
+// */
+// /*private void notifyDistanceUpdateToUI(final String positionId) {
+// if (Looper.myLooper() == Looper.getMainLooper()) {
+// if (mDistanceReceiver != null) {
+// mDistanceReceiver.onDistanceUpdate(positionId);
+// }
+// } else {
+// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
+// @Override
+// public void run() {
+// if (mDistanceReceiver != null) {
+// mDistanceReceiver.onDistanceUpdate(positionId);
+// }
+// }
+// });
+// }
+// }*/
+//
+//
+//
+//
+//
+//// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
+//
+// public ArrayList getPositionList() {
+// if (!isServiceRunning) {
+// LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
+// return new ArrayList();
+// }
+// return new ArrayList(mPositionList);
+// }
+//
+//
+// public ArrayList getPositionTasksList() {
+// if (!isServiceRunning) {
+// LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
+// return new ArrayList();
+// }
+// return new ArrayList(mTaskList);
+// }
+//
+//
+//
+//
+// /*public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
+// this.mDistanceReceiver = receiver;
+// LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + ")");
+// }*/
+//
+// public void addVisibleDistanceView(String positionId) {
+// if (!isServiceRunning || positionId == null) {
+// LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
+// return;
+// }
+// if (mVisiblePositionIds.add(positionId)) {
+// LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
+//// 新增:添加可见位置后,立即更新通知(显示最新可见数量)
+// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
+// syncGpsStatusToNotification();
+// }
+// }
+// }
+//
+// public void removeVisibleDistanceView(String positionId) {
+// if (positionId == null) {
+// LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
+// return;
+// }
+// if (mVisiblePositionIds.remove(positionId)) {
+// int remainingCount = mVisiblePositionIds.size();
+// LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + remainingCount + ")");
+//// 新增:移除可见位置后,更新通知(同步数量变化)
+// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
+// syncGpsStatusToNotification();
+// }
+// }
+// }
+//
+// public void clearVisibleDistanceViews() {
+// mVisiblePositionIds.clear();
+// LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
+//// 新增:清空可见位置后,更新通知(提示计算暂停)
+// if (isForegroundServiceStarted) {
+// updateNotificationGpsStatus("无可见位置,距离计算暂停");
+// }
+// }
+//
+//// ---------------------- 数据管理接口(修复原有语法错误+优化逻辑) ----------------------
+// /**
+//
+// - 获取服务运行状态
+// */
+// public boolean isServiceRunning() {
+// return isServiceRunning;
+// }
+//
+// /**
+//
+// - 添加位置(修复迭代器泛型缺失问题)
+// */
+// public void addPosition(PositionModel position) {
+// if (!isServiceRunning || position == null || position.getPositionId() == null) {
+// LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
+// return;
+// }// 修复:显式声明PositionModel泛型,避免类型转换警告
+// boolean isDuplicate = false;
+// Iterator posIter = mPositionList.iterator();
+// while (posIter.hasNext()) {
+// PositionModel existingPos = (PositionModel)posIter.next();
+// if (position.getPositionId().equals(existingPos.getPositionId())) {
+// isDuplicate = true;
+// break;
+// }
+// }if (!isDuplicate) {
+// mPositionList.add(position);
+// LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + ")");
+// } else {
+// LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
+// }
+// }
+//
+// /**
+//
+// - 删除位置(修复任务删除时的类型转换错误)
+// */
+// public void removePosition(String positionId) {
+// if (!isServiceRunning || positionId == null) {
+// LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
+// return;
+// }// 1. 删除位置
+// boolean isRemoved = false;
+// Iterator posIter = mPositionList.iterator();
+// while (posIter.hasNext()) {
+// PositionModel pos = (PositionModel)posIter.next();
+// if (positionId.equals(pos.getPositionId())) {
+// posIter.remove();
+// isRemoved = true;
+// break;
+// }
+// }if (isRemoved) {
+//// 修复:任务列表迭代时用PositionTaskModel泛型(原错误用PositionModel导致转换失败)
+// Iterator taskIter = mTaskList.iterator();
+// while (taskIter.hasNext()) {
+// PositionTaskModel task = (PositionTaskModel)taskIter.next();
+// if (positionId.equals(task.getPositionId())) {
+// taskIter.remove();
+// }
+// }// 3. 移除可见位置
+// mVisiblePositionIds.remove(positionId);
+// LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ",剩余任务数=" + mTaskList.size() + ")");
+// } else {
+// LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
+// }
+// }
+//
+// /**
+//
+// - 更新位置信息(修复代码格式+迭代器泛型)
+// */
+// public void updatePosition(PositionModel updatedPosition) {
+// if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
+// LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
+// return;
+// }boolean isUpdated = false;
+// Iterator posIter = mPositionList.iterator();
+// while (posIter.hasNext()) {
+// PositionModel pos = (PositionModel)posIter.next();
+// if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
+// pos.setMemo(updatedPosition.getMemo());
+// pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
+// if (!updatedPosition.isEnableRealPositionDistance()) {
+// pos.setRealPositionDistance(-1);
+// //notifyDistanceUpdateToUI(pos.getPositionId());
+// }
+// isUpdated = true;
+// break;
+// }
+// }if (isUpdated) {
+// LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + ")");
+// } else {
+// LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
+// }
+// }
+//
+// /**
+//
+// - 同步任务列表(修复泛型缺失+代码格式)
+// */
+// public void syncAllPositionTasks(ArrayList tasks) {
+// if (!isServiceRunning || tasks == null) {
+// LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
+// return;
+// }// 1. 清空旧任务
+// mTaskList.clear();
+//// 2. 添加新任务(修复泛型+去重逻辑)
+// Set taskIdSet = new HashSet();
+// Iterator taskIter = tasks.iterator();
+// while (taskIter.hasNext()) {
+// PositionTaskModel task = (PositionTaskModel)taskIter.next();
+// if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
+// taskIdSet.add(task.getTaskId());
+// mTaskList.add(task);
+// }
+// }LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + ")");
+// }
+//
+//
+//
+//
+//// ---------------------- 补充:修复LocationProvider引用缺失问题(避免编译错误) ----------------------
+//// 注:原代码中onStatusChanged使用LocationProvider枚举,需补充静态导入或显式声明
+//// 此处通过内部静态类定义,解决系统API引用问题(兼容Java 7语法)
+// private static class LocationProvider {
+// public static final int AVAILABLE = 2;
+// public static final int OUT_OF_SERVICE = 0;
+// public static final int TEMPORARILY_UNAVAILABLE = 1;
+// }
+//
+//// ---------------------- 补充:Context引用工具(避免服务销毁后Context失效) ----------------------
+// /*private Context getSafeContext() {
+// // 服务未销毁时返回自身Context,已销毁时返回应用Context(避免内存泄漏)
+// if (isDestroyed()) {
+// return getApplicationContext();
+// }
+// return this;
+// }*/
+//
+//// 注:isDestroyed()为API 17+方法,若需兼容更低版本,可添加版本判断
+// /*private boolean isDestroyed() {
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+// return super.isDestroyed();
+// }
+// // 低版本通过状态标记间接判断(服务销毁时会置为false)
+// return !isServiceRunning && !isForegroundServiceStarted;
+// }*/
+//}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/services/MainService.java b/positions/src/main/java/cc/winboll/studio/positions/services/MainService.java
new file mode 100644
index 00000000..59b9af08
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/services/MainService.java
@@ -0,0 +1,1089 @@
+package cc.winboll.studio.positions.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2024/07/19 14:30:57
+ * @Describe 应用主要服务组件类
+ */
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.positions.models.PositionModel;
+import cc.winboll.studio.positions.models.PositionTaskModel;
+import cc.winboll.studio.positions.utils.AppConfigsUtil;
+import cc.winboll.studio.positions.utils.NotificationUtil;
+import cc.winboll.studio.positions.utils.ServiceUtil;
+import com.hjq.toast.ToastUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class MainService extends Service {
+
+ public static final String TAG = "MainService";
+
+ // GPS监听接口(Java 7 标准接口定义,无Lambda依赖)
+ public interface GpsUpdateListener {
+ void onGpsPositionUpdated(PositionModel currentGpsPos);
+ void onGpsStatusChanged(String status);
+ }
+
+ // 任务更新监听接口(Java 7 风格,供Adapter监听任务变化)
+ public interface TaskUpdateListener {
+ void onTaskUpdated();
+ }
+
+ // 监听管理(弱引用+线程安全集合,适配Java 7,避免内存泄漏+并发异常)
+ private final Set> mGpsListeners = new HashSet>();
+ private final Set> mTaskListeners = new HashSet>();
+ private final Object mListenerLock = new Object(); // 监听操作锁,保证线程安全
+
+ // 原有核心变量(Java 7 显式初始化,无Java 8+语法)
+ private LocalBinder mLocalBinder; //持有 LocalBinder 实例(用于暴露服务)
+ private LocationManager mLocationManager;
+ private LocationListener mGpsLocationListener;
+ private static final long GPS_UPDATE_INTERVAL = 2000; // GPS更新间隔:2秒
+ private static final float GPS_UPDATE_DISTANCE = 1; // GPS更新距离阈值:1米
+ private boolean isGpsEnabled = false; // GPS是否启用标记
+ private boolean isGpsPermissionGranted = false; // 定位权限是否授予标记
+
+ // 数据存储集合(Java 7 基础集合,避免Stream/forEach等Java 8+特性)
+ private final ArrayList mPositionList = new ArrayList(); // 位置数据列表
+ private final ArrayList mTaskList = new ArrayList();// 任务数据列表
+ private PositionModel mCurrentGpsPosition; // 当前GPS定位数据
+
+ // 服务相关变量(Java 7 显式声明,保持原逻辑)
+ MyServiceConnection mMyServiceConnection;
+ volatile static boolean _mIsServiceRunning; // 服务运行状态(volatile保证可见性)
+ AppConfigsUtil mAppConfigsUtil;
+ private ScheduledExecutorService distanceExecutor = Executors.newSingleThreadScheduledExecutor(); // 单线程池处理距离计算
+ private final Set mVisiblePositionIds = new HashSet(); // 可见位置ID集合
+
+ // 单例+应用上下文(Java 7 静态变量,保证服务实例唯一+上下文安全)
+ private static volatile MainService sInstance;
+ private static Context sAppContext;
+
+
+ // =========================================================================
+ // 任务操作核心接口(Java 7 实现,全迭代器遍历,无ConcurrentModificationException)
+ // =========================================================================
+ /**
+ * 新增任务(Adapter调用,通过MainService统一管理任务,保证数据一致性)
+ * @param newTask 待新增的任务模型
+ */
+ public void addPositionTask(PositionTaskModel newTask) {
+ // 参数校验(Java 7 基础判断,无Optional等Java 8+特性)
+ if (newTask == null || TextUtils.isEmpty(newTask.getPositionId())) {
+ LogUtils.w(TAG, "addPositionTask:任务为空或未绑定位置ID,新增失败");
+ return;
+ }
+
+ // 任务去重(Java 7 迭代器遍历,避免增强for循环删除/新增导致的并发异常)
+ boolean isDuplicate = false;
+ Iterator taskIter = mTaskList.iterator();
+ while (taskIter.hasNext()) {
+ PositionTaskModel task = taskIter.next();
+ if (newTask.getTaskId().equals(task.getTaskId())) {
+ isDuplicate = true;
+ break;
+ }
+ }
+ if (isDuplicate) {
+ LogUtils.w(TAG, "addPositionTask:任务ID已存在(" + newTask.getTaskId() + "),新增失败");
+ return;
+ }
+
+ // 新增任务+持久化+通知刷新(全Java 7 语法)
+ mTaskList.add(newTask);
+ saveTaskList();
+ notifyTaskUpdated(); // 通知所有监听者(如Adapter)任务已更新
+ LogUtils.d(TAG, "addPositionTask:成功(位置ID=" + newTask.getPositionId() + ",任务ID=" + newTask.getTaskId() + ")");
+ }
+
+ /**
+ * 获取指定位置的所有任务(Adapter显示任务数量用,数据来源唯一)
+ * @param positionId 位置ID
+ * @return 该位置绑定的所有任务(返回新列表,避免外部修改原数据)
+ */
+ public ArrayList getTasksByPositionId(String positionId) {
+ ArrayList posTasks = new ArrayList();
+ if (TextUtils.isEmpty(positionId) || mTaskList.isEmpty()) {
+ return posTasks;
+ }
+
+ // 筛选任务(Java 7 迭代器遍历,安全筛选)
+ Iterator taskIter = mTaskList.iterator();
+ while (taskIter.hasNext()) {
+ PositionTaskModel task = taskIter.next();
+ if (positionId.equals(task.getPositionId())) {
+ posTasks.add(task);
+ }
+ }
+ return posTasks;
+ }
+
+ /**
+ * 获取所有任务(Adapter全量刷新用,返回拷贝避免原数据被外部修改)
+ * @return 所有任务的拷贝列表
+ */
+ public ArrayList getAllTasks() {
+ return new ArrayList(mTaskList); // Java 7 集合拷贝方式
+ }
+
+ /**
+ * 删除任务(Adapter调用,通过迭代器安全删除,避免并发异常)
+ * @param taskId 待删除任务的ID
+ */
+ public void deletePositionTask(String taskId) {
+ if (TextUtils.isEmpty(taskId) || mTaskList.isEmpty()) {
+ LogUtils.w(TAG, "deletePositionTask:任务ID为空或列表为空,删除失败");
+ return;
+ }
+
+ // 迭代器删除(Java 7 唯一安全删除集合元素的方式)
+ Iterator taskIter = mTaskList.iterator();
+ while (taskIter.hasNext()) {
+ PositionTaskModel task = taskIter.next();
+ if (taskId.equals(task.getTaskId())) {
+ taskIter.remove(); // 迭代器安全删除,无ConcurrentModificationException
+ saveTaskList();
+ notifyTaskUpdated();
+ LogUtils.d(TAG, "deletePositionTask:成功(任务ID=" + taskId + ")");
+ break;
+ }
+ }
+ }
+
+ /**
+ * 注册任务更新监听(Java 7 弱引用管理,避免内存泄漏)
+ * @param listener 任务更新监听者(如Adapter)
+ */
+ public void registerTaskUpdateListener(TaskUpdateListener listener) {
+ if (listener == null) {
+ LogUtils.w(TAG, "registerTaskUpdateListener:监听者为空,跳过");
+ return;
+ }
+ synchronized (mListenerLock) { // 加锁保证多线程注册安全
+ mTaskListeners.add(new WeakReference(listener));
+ }
+ }
+
+ /**
+ * 反注册任务更新监听(Java 7 迭代器清理,避免内存泄漏)
+ * @param listener 待反注册的监听者
+ */
+ public void unregisterTaskUpdateListener(TaskUpdateListener listener) {
+ if (listener == null) {
+ LogUtils.w(TAG, "unregisterTaskUpdateListener:监听者为空,跳过");
+ return;
+ }
+ synchronized (mListenerLock) {
+ Iterator> iter = mTaskListeners.iterator();
+ while (iter.hasNext()) {
+ WeakReference ref = iter.next();
+ // 清理目标监听者或已被回收的弱引用
+ if (ref.get() == listener || ref.get() == null) {
+ iter.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * 通知所有任务监听者更新(Java 7 匿名内部类实现主线程回调,无Lambda)
+ */
+ private void notifyTaskUpdated() {
+ synchronized (mListenerLock) {
+ Iterator> iter = mTaskListeners.iterator();
+ while (iter.hasNext()) {
+ final WeakReference ref = iter.next();
+ if (ref.get() != null) {
+ // 判断是否在主线程,不在则切换(Java 7 匿名Runnable,无Lambda)
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ ref.get().onTaskUpdated();
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ ref.get().onTaskUpdated();
+ }
+ });
+ }
+ } else {
+ iter.remove(); // 清理已回收的弱引用,避免内存泄漏
+ }
+ }
+ }
+ }
+
+
+ // =========================================================================
+ // 原有基础方法(Java 7 语法调整:移除所有Lambda/方法引用,用匿名内部类替代)
+ // =========================================================================
+ /**
+ * 获取服务单例(Java 7 静态同步方法,保证线程安全)
+ * @param context 上下文
+ * @return MainService实例(未绑定成功时返回null)
+ */
+ public static synchronized MainService getInstance(Context context) {
+ if (sInstance == null) {
+ Intent intent = new Intent(context.getApplicationContext(), MainService.class);
+ context.getApplicationContext().startService(intent);
+ return null;
+ }
+ if (sAppContext == null) {
+ sAppContext = sInstance.getApplicationContext();
+ }
+ return sInstance;
+ }
+
+ /**
+ * 服务绑定回调(Java 7 基础实现,无默认方法等Java 8+特性)
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ // 返回 LocalBinder,使Activity能通过Binder获取MainService实例
+ return mLocalBinder;
+ }
+
+ /**
+ * 服务创建回调(初始化单例、上下文、配置、服务连接等)
+ */
+ @Override
+ public void onCreate() {
+ LogUtils.d(TAG, "onCreate");
+ super.onCreate();
+ sInstance = this;
+ sAppContext = getApplicationContext();
+
+ // 初始化 LocalBinder(关键:将MainService实例传入Binder)
+ mLocalBinder = new LocalBinder(this);
+
+ _mIsServiceRunning = false;
+ mAppConfigsUtil = AppConfigsUtil.getInstance(this);
+
+ // 初始化服务连接(Java 7 显式判断,无Optional)
+ if (mMyServiceConnection == null) {
+ mMyServiceConnection = new MyServiceConnection();
+ }
+
+ run(); // 启动服务核心逻辑
+ }
+
+ /**
+ * 服务核心逻辑(启动前台服务、初始化GPS、加载数据等)
+ */
+ public void run() {
+ if (mAppConfigsUtil.isEnableMainService(true)) {
+ if (!_mIsServiceRunning) {
+ _mIsServiceRunning = true;
+ wakeupAndBindAssistant(); // 唤醒并绑定辅助服务
+
+ // 启动前台服务(Java 7 显式调用,无方法引用)
+ String initialStatus = "[ Positions ] is in Service.";
+ NotificationUtil.createForegroundServiceNotification(this, initialStatus);
+ startForeground(NotificationUtil.FOREGROUND_SERVICE_NOTIFICATION_ID,
+ NotificationUtil.createForegroundServiceNotification(this, initialStatus));
+
+ // 初始化GPS相关(Java 7 基础API调用)
+ mLocationManager = (LocationManager) sInstance.getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
+ initGpsLocationListener();
+ startGpsLocation();
+
+ // 加载本地数据(Java 7 静态方法调用,无方法引用)
+ PositionModel.loadBeanList(MainService.this, mPositionList, PositionModel.class);
+ PositionTaskModel.loadBeanList(MainService.this, mTaskList, PositionTaskModel.class);
+
+ // 提示与日志(Java 7 基础调用)
+ ToastUtils.show(initialStatus);
+ LogUtils.i(TAG, initialStatus);
+ }
+ }
+ }
+
+ /**
+ * 获取服务运行状态
+ * @return true=运行中,false=未运行
+ */
+ public boolean isServiceRunning() {
+ return _mIsServiceRunning;
+ }
+
+ /**
+ * 服务销毁回调(清理资源、停止GPS、清空数据、反注册监听等)
+ */
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ sInstance = null;
+
+ // 清理资源(Java 7 顺序调用,无Stream等特性)
+ stopGpsLocation();
+ clearAllData();
+ stopForeground(true);
+
+ // 清理所有监听者(Java 7 加锁+清空,避免内存泄漏)
+ synchronized (mListenerLock) {
+ mGpsListeners.clear();
+ mTaskListeners.clear();
+ }
+
+ // 重置状态变量
+ _mIsServiceRunning = false;
+ isGpsEnabled = false;
+ mLocationManager = null;
+ }
+
+
+ // =========================================================================
+ // 位置操作方法(Java 7 语法,全迭代器/基础循环,无Java 8+特性)
+ // =========================================================================
+ /**
+ * 获取所有位置数据(返回原列表,供外部读取)
+ * @return 位置列表
+ */
+ public ArrayList getPositionList() {
+ return mPositionList;
+ }
+
+ /**
+ * 获取当前GPS位置
+ * @return 当前GPS定位模型(未获取时返回null)
+ */
+ public PositionModel getCurrentGpsPosition() {
+ return mCurrentGpsPosition;
+ }
+
+ /**
+ * 删除指定位置(Java 7 迭代器安全删除)
+ * @param targetPosId 待删除位置的ID
+ */
+ public void removePosition(String targetPosId) {
+ if (TextUtils.isEmpty(targetPosId) || mPositionList.isEmpty()) {
+ LogUtils.w(TAG, "removePosition:参数无效");
+ return;
+ }
+ // 迭代器遍历删除(Java 7 安全方式)
+ Iterator iter = mPositionList.iterator();
+ while (iter.hasNext()) {
+ PositionModel pos = iter.next();
+ if (targetPosId.equals(pos.getPositionId())) {
+ iter.remove();
+ savePositionList();
+ break;
+ }
+ }
+ }
+
+ /**
+ * 更新位置数据(Java 7 基础for循环,无Stream筛选)
+ * @param updatedPos 更新后的位置模型
+ */
+ public void updatePosition(PositionModel updatedPos) {
+ if (updatedPos == null || TextUtils.isEmpty(updatedPos.getPositionId()) || mPositionList.isEmpty()) {
+ LogUtils.w(TAG, "updatePosition:参数无效");
+ return;
+ }
+ // 基础for循环查找并更新(Java 7 标准写法)
+ for (int i = 0; i < mPositionList.size(); i++) {
+ PositionModel oldPos = mPositionList.get(i);
+ if (updatedPos.getPositionId().equals(oldPos.getPositionId())) {
+ mPositionList.set(i, updatedPos);
+ savePositionList();
+ break;
+ }
+ }
+ }
+
+ /**
+ * 同步所有位置任务(全量替换,用于批量更新)
+ * @param newTaskList 新的任务列表
+ */
+ public void syncAllPositionTasks(ArrayList newTaskList) {
+ if (newTaskList == null) {
+ LogUtils.w(TAG, "syncAllPositionTasks:新列表为空");
+ return;
+ }
+ // 全量替换+持久化+通知(Java 7 基础集合操作)
+ mTaskList.clear();
+ mTaskList.addAll(newTaskList);
+ saveTaskList();
+ notifyTaskUpdated();
+ }
+
+ /**
+ * 新增位置(Java 7 增强for循环去重,无Stream)
+ * @param newPos 待新增的位置模型
+ */
+ public void addPosition(PositionModel newPos) {
+ if (newPos == null) {
+ LogUtils.w(TAG, "addPosition:位置为空");
+ return;
+ }
+ // 位置去重(Java 7 增强for循环,无Stream.filter)
+ boolean isDuplicate = false;
+ for (PositionModel pos : mPositionList) {
+ if (newPos.getPositionId().equals(pos.getPositionId())) {
+ isDuplicate = true;
+ break;
+ }
+ }
+ if (!isDuplicate) {
+ mPositionList.add(newPos);
+ savePositionList();
+ }
+ }
+
+ /**
+ * 持久化位置数据(Java 7 静态方法调用,保持原逻辑)
+ */
+ void savePositionList() {
+ LogUtils.d(TAG, String.format("savePositionList : size=%d", mPositionList.size()));
+ PositionModel.saveBeanList(MainService.this, mPositionList, PositionModel.class);
+ }
+
+ /**
+ * 持久化任务数据(Java 7 静态方法调用,保持原逻辑)
+ */
+ void saveTaskList() {
+ LogUtils.d(TAG, String.format("saveTaskList : size=%d", mTaskList.size()));
+ PositionTaskModel.saveBeanList(MainService.this, mTaskList, PositionTaskModel.class);
+ }
+
+ /**
+ * 清空所有数据(位置+任务+GPS缓存,Java 7 集合clear方法)
+ */
+ public void clearAllData() {
+ mPositionList.clear();
+ mTaskList.clear();
+ mCurrentGpsPosition = null;
+ LogUtils.d(TAG, "clearAllData:已清空所有数据");
+ }
+
+ /**
+ * 同步当前GPS位置(更新缓存+通知监听者+同步通知栏,全Java 7 语法)
+ * @param position 最新GPS位置模型
+ */
+ public void syncCurrentGpsPosition(PositionModel position) {
+ if (position == null) {
+ LogUtils.w(TAG, "syncCurrentGpsPosition:位置为空");
+ return;
+ }
+ this.mCurrentGpsPosition = position;
+ LogUtils.d(TAG, "syncCurrentGpsPosition:成功(纬度=" + position.getLatitude() + ",经度=" + position.getLongitude() + ")");
+ notifyAllGpsListeners(position);
+
+ // 服务运行中才同步通知栏状态
+ if (_mIsServiceRunning) {
+ syncGpsStatusToNotification();
+ }
+ }
+
+ /**
+ * 同步GPS状态到前台通知(Java 7 匿名Runnable切换主线程,无Lambda)
+ */
+ private void syncGpsStatusToNotification() {
+ if (!_mIsServiceRunning || mCurrentGpsPosition == null) {
+ return;
+ }
+ // 格式化通知内容(Java 7 String.format,无String.join等Java 8+方法)
+ final String gpsStatus = String.format(
+ "GPS位置:北纬%.4f° 东经%.4f° | 可见位置:%d个",
+ mCurrentGpsPosition.getLatitude(),
+ mCurrentGpsPosition.getLongitude(),
+ mVisiblePositionIds.size()
+ );
+ // 主线程判断+切换(Java 7 匿名内部类)
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ NotificationUtil.updateForegroundServiceStatus(this, gpsStatus);
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ NotificationUtil.updateForegroundServiceStatus(MainService.this, gpsStatus);
+ }
+ });
+ }
+ }
+
+ /**
+ * 计算可见位置距离(原逻辑保留,Java 7 语法兼容,无Stream/并行流)
+ */
+ private void calculateVisiblePositionDistance() {
+ // 原有逻辑(Java 7 语法适配:用迭代器/基础循环,无Lambda/forEach)
+ // 注:原代码标注“略”,此处保持空实现,实际使用时补充具体逻辑
+ }
+
+ /**
+ * 计算两点间距离(Haversine公式,纯Java 7 基础API,无数学工具类依赖)
+ * @param gpsLat GPS纬度
+ * @param gpsLon GPS经度
+ * @param posLat 目标位置纬度
+ * @param posLon 目标位置经度
+ * @return 两点间距离(单位:米)
+ */
+ private double calculateHaversineDistance(double gpsLat, double gpsLon, double posLat, double posLon) {
+ final double EARTH_RADIUS = 6371000; // 地球半径(米)
+ double latDiff = Math.toRadians(posLat - gpsLat);
+ double lonDiff = Math.toRadians(posLon - gpsLon);
+ // Haversine公式核心计算(Java 7 基础数学方法)
+ double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
+ + Math.cos(Math.toRadians(gpsLat)) * Math.cos(Math.toRadians(posLat))
+ * Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return EARTH_RADIUS * c;
+ }
+
+ /**
+ * 强制刷新所有位置距离(GPS更新后调用,计算距离+校验任务触发条件)
+ */
+ public void forceRefreshDistance() {
+ if (mCurrentGpsPosition == null || mPositionList.isEmpty()) {
+ LogUtils.w(TAG, "forceRefreshDistance:GPS未获取或位置为空");
+ return;
+ }
+
+ // 遍历所有位置计算距离(Java 7 增强for循环,无Stream)
+ for (PositionModel pos : mPositionList) {
+ if (pos.isEnableRealPositionDistance()) {
+ try {
+ double distance = calculateHaversineDistance(
+ mCurrentGpsPosition.getLatitude(),
+ mCurrentGpsPosition.getLongitude(),
+ pos.getLatitude(),
+ pos.getLongitude()
+ );
+ pos.setRealPositionDistance(distance);
+ } catch (Exception e) {
+ pos.setRealPositionDistance(-1); // 标记距离计算失败
+ LogUtils.e(TAG, "计算距离失败:" + e.getMessage());
+ }
+ } else {
+ pos.setRealPositionDistance(-1); // 未启用距离计算,标记为无效
+ }
+ }
+
+ // 距离刷新后校验任务触发条件+通知GPS监听者
+ checkAllTaskTriggerCondition();
+ notifyAllGpsListeners(mCurrentGpsPosition);
+ }
+
+
+ // =========================================================================
+ // 任务触发相关方法(Java 7 语法,全迭代器遍历,无Java 8+特性)
+ // =========================================================================
+ /**
+ * 校验所有任务触发条件(距离达标则触发任务通知)
+ */
+ private void checkAllTaskTriggerCondition() {
+ if (mCurrentGpsPosition == null || mPositionList.isEmpty() || mTaskList.isEmpty()) {
+ LogUtils.d(TAG, "checkAllTaskTriggerCondition:跳过校验(GPS/位置/任务为空)");
+ return;
+ }
+
+ LogUtils.d(TAG, "checkAllTaskTriggerCondition:开始校验(任务总数=" + mTaskList.size() + ")");
+ // 迭代器遍历任务(Java 7 安全遍历,避免并发修改异常)
+ Iterator taskIter = mTaskList.iterator();
+ while (taskIter.hasNext()) {
+ PositionTaskModel task = taskIter.next();
+ // 仅校验“已启用”且“绑定有效位置”的任务
+ if (!task.isEnable() || TextUtils.isEmpty(task.getPositionId())) {
+ continue;
+ }
+
+ // 查找任务绑定的位置(Java 7 迭代器遍历位置列表)
+ PositionModel bindPos = null;
+ Iterator posIter = mPositionList.iterator();
+ while (posIter.hasNext()) {
+ PositionModel pos = posIter.next();
+ if (task.getPositionId().equals(pos.getPositionId())) {
+ bindPos = pos;
+ break;
+ }
+ }
+ if (bindPos == null) {
+ LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":绑定位置不存在,跳过");
+ task.setIsBingo(false);
+ continue;
+ }
+
+ // 校验距离条件(判断是否满足任务触发阈值)
+ double currentDistance = bindPos.getRealPositionDistance();
+ if (currentDistance < 0) {
+ LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":距离计算失败,跳过");
+ task.setIsBingo(false);
+ continue;
+ }
+
+ boolean isTriggered = false;
+ int taskDistance = task.getDiscussDistance();
+ // 任务触发条件:大于/小于指定距离(Java 7 基础判断,无三元运算符嵌套)
+ if (task.isGreaterThan()) {
+ isTriggered = currentDistance > taskDistance;
+ } else if (task.isLessThan()) {
+ isTriggered = currentDistance < taskDistance;
+ }
+
+ // 更新任务触发状态+发送通知(状态变化时才处理)
+ if (task.isBingo() != isTriggered) {
+ task.setIsBingo(isTriggered);
+ if (isTriggered) {
+ sendTaskTriggerNotification(task, bindPos, currentDistance);
+ }
+ }
+ }
+ saveTaskList(); // 持久化更新后的任务状态
+ }
+
+ /**
+ * 发送任务触发通知(更新前台通知+显示Toast,Java 7 匿名Runnable切换主线程)
+ * @param task 触发的任务
+ * @param bindPos 任务绑定的位置
+ * @param currentDistance 当前距离
+ */
+ private void sendTaskTriggerNotification(PositionTaskModel task, PositionModel bindPos, double currentDistance) {
+ if (!_mIsServiceRunning) {
+ return;
+ }
+
+ // 格式化通知内容(Java 7 String.format,无TextBlock等Java 15+特性)
+ final String triggerContent = String.format(
+ "任务触发:%s\n位置:%s\n当前距离:%.1f米(条件:%s%d米)",
+ task.getTaskDescription(),
+ bindPos.getMemo(),
+ currentDistance,
+ task.isGreaterThan() ? ">" : "<",
+ task.getDiscussDistance()
+ );
+
+ // 更新前台通知(主线程判断+切换)
+ updateNotificationGpsStatus(triggerContent);
+
+ // 显示Toast(主线程安全调用,Java 7 匿名内部类)
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ ToastUtils.show(triggerContent);
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ ToastUtils.show(triggerContent);
+ }
+ });
+ }
+ LogUtils.i(TAG, "任务触发通知:" + triggerContent);
+ }
+
+
+ // =========================================================================
+ // 服务生命周期+辅助服务相关(Java 7 语法,无Lambda/方法引用)
+ // =========================================================================
+ /**
+ * 服务启动命令(每次startService调用时触发,重启服务核心逻辑)
+ */
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ run(); // 重启服务核心逻辑(保证服务启动后进入运行状态)
+ // 返回START_STICKY:服务被异常杀死后,系统会尝试重启(原逻辑保留)
+ return mAppConfigsUtil.isEnableMainService(true) ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
+ }
+
+ /**
+ * 服务连接内部类(Java 7 静态内部类,避免持有外部类强引用导致内存泄漏)
+ */
+ private class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // 原逻辑保留(空实现,如需绑定辅助服务可补充具体逻辑)
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // 辅助服务断开时,重新唤醒绑定(原逻辑保留)
+ if (mAppConfigsUtil.isEnableMainService(true)) {
+ wakeupAndBindAssistant();
+ }
+ }
+ }
+
+ /**
+ * 唤醒并绑定辅助服务(检查服务状态,未存活则启动+绑定)
+ */
+ void wakeupAndBindAssistant() {
+ // 检查辅助服务是否存活(Java 7 静态方法调用,无方法引用)
+ if (!ServiceUtil.isServiceAlive(getApplicationContext(), AssistantService.class.getName())) {
+ // 启动+绑定辅助服务(Java 7 显式Intent,无Lambda)
+ startService(new Intent(MainService.this, AssistantService.class));
+ bindService(new Intent(MainService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
+ }
+ }
+
+
+ // =========================================================================
+ // GPS相关核心方法(Java 7 语法,匿名内部类实现LocationListener,无Lambda)
+ // =========================================================================
+ /**
+ * 构造函数(Java 7 显式初始化线程池+GPS监听器,无默认构造函数简化)
+ */
+ public MainService() {
+ distanceExecutor = Executors.newSingleThreadScheduledExecutor();
+ initGpsLocationListener();
+ }
+
+ /**
+ * 初始化GPS监听器(Java 7 匿名内部类实现LocationListener,无Lambda)
+ */
+ private void initGpsLocationListener() {
+ LogUtils.d(TAG, "initGpsLocationListener");
+ mGpsLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ if (location != null) {
+ // 封装GPS位置为PositionModel(Java 7 显式setter调用)
+ PositionModel gpsPos = new PositionModel();
+ gpsPos.setLatitude(location.getLatitude());
+ gpsPos.setLongitude(location.getLongitude());
+ gpsPos.setPositionId("CURRENT_GPS_POS");
+ gpsPos.setMemo("实时GPS位置");
+
+ // 同步GPS位置+刷新距离+日志(原逻辑保留)
+ syncCurrentGpsPosition(gpsPos);
+ forceRefreshDistance();
+ LogUtils.d(TAG, "GPS位置更新:纬度=" + location.getLatitude() + ",经度=" + location.getLongitude());
+ }
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ // 仅处理GPS_PROVIDER状态变化(Java 7 基础判断)
+ if (provider.equals(LocationManager.GPS_PROVIDER)) {
+ String statusDesc = "";
+ // 状态枚举判断(Java 7 switch,无增强switch)
+ switch (status) {
+ case LocationProvider.AVAILABLE:
+ statusDesc = "GPS状态:已就绪(可用)";
+ break;
+ case LocationProvider.OUT_OF_SERVICE:
+ statusDesc = "GPS状态:无服务(信号弱)";
+ break;
+ case LocationProvider.TEMPORARILY_UNAVAILABLE:
+ statusDesc = "GPS状态:临时不可用(遮挡)";
+ break;
+ }
+ LogUtils.d(TAG, statusDesc);
+ notifyAllGpsStatusListeners(statusDesc);
+ updateNotificationGpsStatus(statusDesc);
+ }
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ // GPS启用时更新状态+通知+重启定位(Java 7 基础逻辑)
+ if (provider.equals(LocationManager.GPS_PROVIDER)) {
+ isGpsEnabled = true;
+ String statusDesc = "GPS已开启(用户手动打开)";
+ LogUtils.d(TAG, statusDesc);
+ notifyAllGpsStatusListeners(statusDesc);
+ updateNotificationGpsStatus("GPS已开启,正在获取位置...");
+ startGpsLocation();
+ }
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ // GPS禁用时清空状态+通知+提示(Java 7 基础逻辑)
+ if (provider.equals(LocationManager.GPS_PROVIDER)) {
+ isGpsEnabled = false;
+ mCurrentGpsPosition = null;
+ String statusDesc = "GPS已关闭(用户手动关闭)";
+ LogUtils.w(TAG, statusDesc);
+ notifyAllGpsStatusListeners(statusDesc);
+ updateNotificationGpsStatus("GPS已关闭,请在设置中开启");
+ ToastUtils.show("GPS已关闭,无法获取位置,请在设置中开启");
+ }
+ }
+ };
+ }
+
+ /**
+ * 检查GPS就绪状态(权限+启用状态,Java 7 基础权限判断,无Stream)
+ * @return true=GPS就绪,false=未就绪
+ */
+ private boolean checkGpsReady() {
+ // 检查定位权限(Java 7 基础权限API,无权限请求框架依赖)
+ isGpsPermissionGranted = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+
+ // 初始化LocationManager(Java 7 显式判断,无Optional)
+ if (mLocationManager == null) {
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ }
+ // 检查GPS是否启用(系统LocationManager API,Java 7 兼容)
+ isGpsEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+
+ // 权限未授予:提示+日志+通知
+ if (!isGpsPermissionGranted) {
+ String tip = "GPS准备失败:缺少精确定位权限";
+ LogUtils.e(TAG, tip);
+ notifyAllGpsStatusListeners(tip);
+ updateNotificationGpsStatus("缺少定位权限,无法获取GPS");
+ ToastUtils.show("请授予定位权限,否则无法获取GPS位置");
+ return false;
+ }
+ // GPS未启用:提示+日志+通知
+ if (!isGpsEnabled) {
+ String tip = "GPS准备失败:系统GPS未开启";
+ LogUtils.e(TAG, tip);
+ notifyAllGpsStatusListeners(tip);
+ updateNotificationGpsStatus("GPS未开启,请在设置中打开");
+ ToastUtils.show("GPS已关闭,请在设置中开启以获取位置");
+ return false;
+ }
+
+ LogUtils.d(TAG, "GPS准备就绪:权限已获取,GPS已开启");
+ return true;
+ }
+
+ /**
+ * 启动GPS定位(Java 7 异常处理,无try-with-resources,显式捕获SecurityException)
+ */
+ private void startGpsLocation() {
+ if (!checkGpsReady()) {
+ return;
+ }
+
+ try {
+ // 注册GPS位置更新(Java 7 标准LocationManager API,指定Looper为主线程)
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ GPS_UPDATE_INTERVAL,
+ GPS_UPDATE_DISTANCE,
+ mGpsLocationListener,
+ Looper.getMainLooper()
+ );
+
+ // 获取最后已知GPS位置(缓存位置,避免首次定位等待)
+ Location lastKnownLocation = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ if (lastKnownLocation != null) {
+ PositionModel lastGpsPos = new PositionModel();
+ lastGpsPos.setLatitude(lastKnownLocation.getLatitude());
+ lastGpsPos.setLongitude(lastKnownLocation.getLongitude());
+ lastGpsPos.setPositionId("CURRENT_GPS_POS");
+ syncCurrentGpsPosition(lastGpsPos);
+ LogUtils.d(TAG, "已获取缓存GPS位置:纬度=" + lastKnownLocation.getLatitude());
+ } else {
+ String tip = "无缓存GPS位置,等待实时定位...";
+ LogUtils.d(TAG, tip);
+ notifyAllGpsStatusListeners(tip);
+ updateNotificationGpsStatus("GPS搜索中(请移至开阔地带)");
+ }
+
+ } catch (SecurityException e) {
+ // 定位权限异常(Java 7 显式捕获,无Lambda异常处理)
+ String error = "启动GPS失败(权限异常):" + e.getMessage();
+ LogUtils.e(TAG, error);
+ notifyAllGpsStatusListeners(error);
+ isGpsPermissionGranted = false;
+ updateNotificationGpsStatus("定位权限异常,无法获取GPS");
+ } catch (Exception e) {
+ // 其他异常(如LocationManager为空、系统服务异常等)
+ String error = "启动GPS失败:" + e.getMessage();
+ LogUtils.e(TAG, error);
+ notifyAllGpsStatusListeners(error);
+ updateNotificationGpsStatus("GPS启动失败,尝试重试...");
+ }
+ }
+
+ /**
+ * 停止GPS定位(Java 7 异常处理,移除监听器避免内存泄漏)
+ */
+ private void stopGpsLocation() {
+ // 校验参数:避免空指针+权限未授予时调用
+ if (mLocationManager != null && mGpsLocationListener != null && isGpsPermissionGranted) {
+ try {
+ mLocationManager.removeUpdates(mGpsLocationListener);
+ String tip = "GPS定位已停止(移除监听器)";
+ LogUtils.d(TAG, tip);
+ notifyAllGpsStatusListeners(tip);
+ } catch (Exception e) {
+ String error = "停止GPS失败:" + e.getMessage();
+ LogUtils.e(TAG, error);
+ notifyAllGpsStatusListeners(error);
+ }
+ }
+ }
+
+ /**
+ * 更新前台通知的GPS状态(Java 7 主线程切换,匿名Runnable实现)
+ * @param statusText 通知显示的状态文本
+ */
+ private void updateNotificationGpsStatus(final String statusText) {
+ if (_mIsServiceRunning) {
+ // 判断当前线程是否为主线程,避免UI操作在子线程
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ NotificationUtil.updateForegroundServiceStatus(this, statusText);
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ NotificationUtil.updateForegroundServiceStatus(MainService.this, statusText);
+ }
+ });
+ }
+ }
+ }
+
+
+ // =========================================================================
+ // GPS监听通知相关方法(Java 7 迭代器遍历弱引用集合,避免内存泄漏)
+ // =========================================================================
+ /**
+ * 通知所有GPS监听者位置更新(Java 7 迭代器+弱引用管理,无Stream)
+ * @param currentGpsPos 当前最新GPS位置
+ */
+ private void notifyAllGpsListeners(PositionModel currentGpsPos) {
+ if (currentGpsPos == null || mGpsListeners.isEmpty()) {
+ return;
+ }
+ synchronized (mListenerLock) {
+ Iterator> iter = mGpsListeners.iterator();
+ while (iter.hasNext()) {
+ WeakReference ref = iter.next();
+ GpsUpdateListener listener = ref.get();
+ if (listener != null) {
+ notifySingleListener(listener, currentGpsPos);
+ } else {
+ iter.remove(); // 清理已被GC回收的监听者,避免内存泄漏
+ }
+ }
+ }
+ }
+
+ /**
+ * 通知单个GPS监听者位置更新(主线程安全,Java 7 匿名Runnable)
+ * @param listener 单个监听者
+ * @param currentGpsPos 当前GPS位置
+ */
+ private void notifySingleListener(final GpsUpdateListener listener, final PositionModel currentGpsPos) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ listener.onGpsPositionUpdated(currentGpsPos);
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onGpsPositionUpdated(currentGpsPos);
+ }
+ });
+ }
+ }
+
+ /**
+ * 通知所有GPS监听者状态变化(如GPS开启/关闭、信号弱等,Java 7 迭代器)
+ * @param status GPS状态描述文本
+ */
+ private void notifyAllGpsStatusListeners(final String status) {
+ if (status == null || mGpsListeners.isEmpty()) {
+ return;
+ }
+ synchronized (mListenerLock) {
+ Iterator> iter = mGpsListeners.iterator();
+ while (iter.hasNext()) {
+ WeakReference ref = iter.next();
+ final GpsUpdateListener listener = ref.get();
+ if (listener != null) {
+ // 主线程切换,避免监听者在子线程处理UI
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ listener.onGpsStatusChanged(status);
+ } else {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onGpsStatusChanged(status);
+ }
+ });
+ }
+ } else {
+ iter.remove(); // 清理无效弱引用
+ }
+ }
+ }
+ }
+
+ /**
+ * 注册GPS更新监听(Java 7 弱引用添加,避免监听者内存泄漏)
+ * @param listener GPS更新监听者(如Activity/Adapter)
+ */
+ public void registerGpsUpdateListener(GpsUpdateListener listener) {
+ if (listener == null) {
+ LogUtils.w(TAG, "registerGpsUpdateListener:监听者为空");
+ return;
+ }
+ synchronized (mListenerLock) {
+ mGpsListeners.add(new WeakReference(listener));
+ LogUtils.d(TAG, "GPS监听注册成功,当前数量:" + mGpsListeners.size());
+ // 注册后立即推送当前GPS位置(避免监听者错过初始数据)
+ if (mCurrentGpsPosition != null) {
+ notifySingleListener(listener, mCurrentGpsPosition);
+ }
+ }
+ }
+
+ /**
+ * 反注册GPS更新监听(Java 7 迭代器清理,避免内存泄漏)
+ * @param listener 待反注册的GPS监听者
+ */
+ public void unregisterGpsUpdateListener(GpsUpdateListener listener) {
+ if (listener == null) {
+ LogUtils.w(TAG, "unregisterGpsUpdateListener:监听者为空");
+ return;
+ }
+ synchronized (mListenerLock) {
+ Iterator> iter = mGpsListeners.iterator();
+ while (iter.hasNext()) {
+ WeakReference ref = iter.next();
+ // 匹配目标监听者或已回收的弱引用,直接移除
+ if (ref.get() == listener || ref.get() == null) {
+ iter.remove();
+ LogUtils.d(TAG, "GPS监听反注册成功,当前数量:" + mGpsListeners.size());
+ break;
+ }
+ }
+ }
+ }
+
+ // 补全 LocalBinder 定义(与 LocationActivity 中的 LocalBinder 保持一致)
+ // 注意:若 LocationActivity 已定义 LocalBinder,此处可删除;建议统一在 MainService 中定义,避免重复
+ public class LocalBinder extends android.os.Binder {
+ private MainService mService;
+
+ public LocalBinder(MainService service) {
+ this.mService = service;
+ }
+
+ public MainService getService() {
+ return mService;
+ }
+ }
+
+}
+
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/utils/AppConfigsUtil.java b/positions/src/main/java/cc/winboll/studio/positions/utils/AppConfigsUtil.java
new file mode 100644
index 00000000..17d3c39b
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/utils/AppConfigsUtil.java
@@ -0,0 +1,68 @@
+package cc.winboll.studio.positions.utils;
+import android.content.Context;
+import cc.winboll.studio.positions.models.AppConfigsModel;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/10/01 12:15
+ * @Describe AppConfigsUtils
+ */
+public class AppConfigsUtil {
+
+ public static final String TAG = "AppConfigsUtils";
+
+ // 1. 私有静态成员变量(volatile 防止指令重排,确保实例初始化完全)
+ private static volatile AppConfigsUtil sInstance;
+ Context mContext;
+ AppConfigsModel mAppConfigsModel;
+
+ // 2. 私有构造方法(禁止外部 new 实例)
+ private AppConfigsUtil(Context context) {
+ // 可选:防止通过反射创建实例(增强单例安全性)
+ if (sInstance != null) {
+ throw new RuntimeException("禁止通过反射创建单例实例");
+ }
+ mContext = context;
+ loadConfigs();
+ }
+
+ // 3. 公开静态方法(双重校验锁,获取唯一实例)
+ public static AppConfigsUtil getInstance(Context context) {
+ // 第一次校验:无锁,快速判断实例是否存在(提升性能)
+ if (sInstance == null) {
+ // 加锁:确保同一时间只有一个线程进入初始化逻辑
+ synchronized (AppConfigsUtil.class) {
+ // 第二次校验:防止多线程并发时重复创建实例(线程安全)
+ if (sInstance == null) {
+ sInstance = new AppConfigsUtil(context);
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ void loadConfigs() {
+ mAppConfigsModel = AppConfigsModel.loadBean(mContext, AppConfigsModel.class);
+ }
+
+ void saveConfigs() {
+ AppConfigsModel.saveBean(mContext, mAppConfigsModel);
+ }
+
+ public boolean isEnableMainService(boolean isReloadConfigs) {
+ if (isReloadConfigs) {
+ loadConfigs();
+ }
+
+ return (mAppConfigsModel == null) ?false: mAppConfigsModel.isEnableMainService();
+ }
+
+ public void setIsEnableMainService(boolean isEnableMainService) {
+ if(mAppConfigsModel == null) {
+ mAppConfigsModel = new AppConfigsModel();
+ }
+ mAppConfigsModel.setIsEnableMainService(isEnableMainService);
+ saveConfigs();
+ }
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/utils/NotificationUtil.java b/positions/src/main/java/cc/winboll/studio/positions/utils/NotificationUtil.java
new file mode 100644
index 00000000..e6d1c494
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/utils/NotificationUtil.java
@@ -0,0 +1,185 @@
+package cc.winboll.studio.positions.utils;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/30 16:09
+ * @Describe NotificationUtils
+ */
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import androidx.core.app.NotificationCompat;
+import cc.winboll.studio.positions.R;
+import cc.winboll.studio.positions.activities.LocationActivity; // 引入你的前台服务类
+
+/**
+ * 通知栏工具类:专注于任务相关通知的显示与点击跳转 + 前台服务通知管理
+ * 核心功能:
+ * 1. 显示任务描述通知,点击后携带positionId/taskId跳转到LocationActivity
+ * 2. 创建前台服务通知(用于DistanceRefreshService保活,符合系统前台服务规范)
+ */
+public class NotificationUtil {
+ public static final String TAG = "NotificationUtils";
+ // 1. 任务通知相关常量(原有)
+ private static final String TASK_NOTIFICATION_CHANNEL_ID = "task_notification_channel_01";
+ private static final String TASK_NOTIFICATION_CHANNEL_NAME = "任务通知";
+ // 2. 前台服务通知新增常量(独立渠道,避免与普通任务通知混淆)
+ private static final String FOREGROUND_SERVICE_CHANNEL_ID = "foreground_location_service_channel_02";
+ private static final String FOREGROUND_SERVICE_CHANNEL_NAME = "位置服务";
+ public static final int FOREGROUND_SERVICE_NOTIFICATION_ID = 10086; // 固定ID(前台服务通知无需动态生成)
+
+ // ---------------------- 原有功能:任务通知(不变) ----------------------
+ private static int getNotificationId(String taskId) {
+ return taskId.hashCode() & 0xFFFFFF;
+ }
+
+ public static void show(Context context, String taskId, String positionId, String taskDescription) {
+ if (context == null || taskId == null || positionId == null) {
+ return;
+ }
+
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ return;
+ }
+
+ createNotificationChannel(notificationManager);
+
+ Intent jumpIntent = new Intent(context, LocationActivity.class);
+ jumpIntent.putExtra("EXTRA_POSITION_ID", positionId);
+ jumpIntent.putExtra("EXTRA_TASK_ID", taskId);
+ jumpIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ getNotificationId(taskId),
+ jumpIntent,
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT :
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ Notification notification = new NotificationCompat.Builder(context, TASK_NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentTitle("任务提醒")
+ .setContentText(taskDescription)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setDefaults(NotificationCompat.DEFAULT_SOUND)
+ .build();
+
+ notificationManager.notify(getNotificationId(taskId), notification);
+ }
+
+ private static void createNotificationChannel(NotificationManager notificationManager) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // 原有:任务通知渠道
+ NotificationChannel taskChannel = new NotificationChannel(
+ TASK_NOTIFICATION_CHANNEL_ID,
+ TASK_NOTIFICATION_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ taskChannel.setDescription("接收任务相关提醒,点击可查看任务详情");
+ taskChannel.enableVibration(true);
+ taskChannel.setVibrationPattern(new long[]{0, 300});
+
+ // 新增:前台服务通知渠道(重要性设为LOW,避免频繁打扰用户)
+ NotificationChannel foregroundChannel = new NotificationChannel(
+ FOREGROUND_SERVICE_CHANNEL_ID,
+ FOREGROUND_SERVICE_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW // 仅通知栏显示,无提示音/震动,符合后台服务低打扰需求
+ );
+ foregroundChannel.setDescription("位置服务运行中,用于后台持续获取GPS数据,关闭会影响定位功能"); // 明确告知用户服务作用
+ foregroundChannel.enableVibration(false); // 前台服务通知不震动(避免打扰)
+ foregroundChannel.setSound(null, null); // 关闭提示音(低打扰)
+
+ // 注册两个渠道(任务+前台服务)
+ notificationManager.createNotificationChannel(taskChannel);
+ notificationManager.createNotificationChannel(foregroundChannel);
+ }
+ }
+
+ public static void cancel(Context context, String taskId) {
+ if (context == null || taskId == null) {
+ return;
+ }
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager != null) {
+ notificationManager.cancel(getNotificationId(taskId));
+ }
+ }
+
+ // ---------------------- 新增核心功能:创建前台服务通知(供DistanceRefreshService调用) ----------------------
+ /**
+ * 创建前台服务通知(符合Android前台服务规范,用于启动/保活DistanceRefreshService)
+ * @param context 服务上下文(直接传入DistanceRefreshService的this即可)
+ * @param serviceStatus 服务状态文本(如“正在后台获取GPS位置...”,动态展示服务状态)
+ * @return 可直接用于startForeground()的Notification对象
+ */
+ public static Notification createForegroundServiceNotification(Context context, String serviceStatus) {
+ // 安全校验:上下文非空(避免服务中调用时的空指针)
+ if (context == null) {
+ throw new IllegalArgumentException("Context cannot be null for foreground service notification");
+ }
+
+ // 步骤1:初始化通知管理器(复用已有逻辑,确保渠道已创建)
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager != null) {
+ createNotificationChannel(notificationManager); // 确保前台服务渠道已注册
+ }
+
+ // 步骤2:构建“点击通知跳转至位置管理页”的Intent(用户点击通知可进入功能页)
+ Intent jumpIntent = new Intent(context, LocationActivity.class);
+ jumpIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); // 避免创建重复页面
+
+ // 步骤3:创建PendingIntent(授权系统在用户点击时执行跳转)
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ FOREGROUND_SERVICE_NOTIFICATION_ID, // 请求码与通知ID一致,确保唯一
+ jumpIntent,
+ // 适配Android 6.0+:IMMUTABLE确保安全性,UPDATE_CURRENT确保Intent参数更新
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT :
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ // 步骤4:构建前台服务通知(低打扰、强关联服务状态)
+ return new NotificationCompat.Builder(context, FOREGROUND_SERVICE_CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher) // 必须设置(系统强制要求,建议用应用图标)
+ .setContentTitle("位置服务运行中") // 固定标题,用户快速识别服务类型
+ .setContentText(serviceStatus) // 动态内容(如“正在获取GPS位置”“已连续运行30分钟”)
+ .setContentIntent(pendingIntent) // 点击跳转至功能页
+ .setOngoing(true) // 关键:设置为“不可手动清除”(仅服务停止时能取消,符合前台服务规范)
+ .setPriority(NotificationCompat.PRIORITY_LOW) // 低优先级:不弹窗、不抢占通知栏焦点
+ .setDefaults(NotificationCompat.DEFAULT_SOUND)
+ .build();
+ }
+
+ /**
+ * (配套工具方法)更新前台服务通知的状态文本(如GPS获取进度、运行时长)
+ * @param context 服务上下文
+ * @param newServiceStatus 新的状态文本(如“已获取最新位置:北纬30.123°”)
+ */
+ public static void updateForegroundServiceStatus(Context context, String newServiceStatus) {
+ if (context == null) {
+ return;
+ }
+ // 重新创建通知(复用create方法,传入新状态文本)
+ Notification updatedNotification = createForegroundServiceNotification(context, newServiceStatus);
+ // 用相同ID更新通知(覆盖旧通知,实现状态刷新)
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager != null) {
+ notificationManager.notify(FOREGROUND_SERVICE_NOTIFICATION_ID, updatedNotification);
+ }
+ }
+}
+
diff --git a/positions/src/main/java/cc/winboll/studio/positions/utils/ServiceUtil.java b/positions/src/main/java/cc/winboll/studio/positions/utils/ServiceUtil.java
new file mode 100644
index 00000000..7b70f7e4
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/utils/ServiceUtil.java
@@ -0,0 +1,34 @@
+package cc.winboll.studio.positions.utils;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2024/07/19 14:30:57
+ * @Describe 应用服务组件工具类
+ */
+import android.app.ActivityManager;
+import android.content.Context;
+import java.util.List;
+
+public class ServiceUtil {
+ public final static String TAG = "ServiceUtil";
+
+ public static boolean isServiceAlive(Context context, String szServiceName) {
+ // 获取Activity管理者对象
+ ActivityManager manager = (ActivityManager) context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ // 获取正在运行的服务(此处设置最多取1000个)
+ List runningServices = manager
+ .getRunningServices(1000);
+ if (runningServices.size() <= 0) {
+ return false;
+ }
+ // 遍历,若存在名字和传入的serviceName的一致则说明存在
+ for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) {
+ if (runningServiceInfo.service.getClassName().equals(szServiceName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java b/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java
new file mode 100644
index 00000000..c9046dc1
--- /dev/null
+++ b/positions/src/main/java/cc/winboll/studio/positions/views/PositionTaskListView.java
@@ -0,0 +1,416 @@
+package cc.winboll.studio.positions.views;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/30 08:09
+ * @Describe 位置任务列表视图(支持简单/编辑模式,含 isBingo 红点标识)
+ */
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import cc.winboll.studio.positions.R;
+import cc.winboll.studio.positions.models.PositionTaskModel;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PositionTaskListView extends LinearLayout {
+ // 视图模式常量
+ public static final int VIEW_MODE_SIMPLE = 1;
+ public static final int VIEW_MODE_EDIT = 2;
+
+ // 核心成员变量
+ private String mBindPositionId;
+ private ArrayList mTaskList;
+ private int mCurrentViewMode;
+ private TaskListAdapter mTaskAdapter;
+ private RecyclerView mRvTasks;
+
+ // 任务修改回调接口
+ public interface OnTaskUpdatedListener {
+ void onTaskUpdated(String positionId, ArrayList updatedTasks);
+ }
+
+ private OnTaskUpdatedListener mOnTaskUpdatedListener;
+
+ // ---------------------- 构造函数 ----------------------
+ public PositionTaskListView(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public PositionTaskListView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public PositionTaskListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ // 初始化视图(绑定控件+设置布局)
+ private void initView(Context context) {
+ setOrientation(VERTICAL);
+ LayoutInflater.from(context).inflate(R.layout.view_position_task_list, this, true);
+
+ mRvTasks = (RecyclerView) findViewById(R.id.rv_position_tasks);
+ mRvTasks.setLayoutManager(new LinearLayoutManager(context));
+
+ mTaskList = new ArrayList();
+ mTaskAdapter = new TaskListAdapter(mTaskList);
+ mRvTasks.setAdapter(mTaskAdapter);
+
+ mCurrentViewMode = VIEW_MODE_SIMPLE;
+ }
+
+ // ---------------------- 对外API ----------------------
+ public void init(ArrayList taskList, String positionId) {
+ this.mBindPositionId = positionId;
+ if (this.mTaskList.isEmpty()) {
+ ArrayList matchedTasks = new ArrayList();
+ if (taskList != null && !taskList.isEmpty()) {
+ for (PositionTaskModel task : taskList) {
+ if (task != null && positionId.equals(task.getPositionId())) {
+ matchedTasks.add(task);
+ }
+ }
+ }
+ mTaskList.clear();
+ mTaskList.addAll(matchedTasks);
+ }
+ mTaskAdapter.notifyDataSetChanged();
+ }
+
+ public void setViewStatus(int viewMode) {
+ if (viewMode != VIEW_MODE_SIMPLE && viewMode != VIEW_MODE_EDIT) {
+ return;
+ }
+ mCurrentViewMode = viewMode;
+ mTaskAdapter.notifyDataSetChanged();
+ }
+
+ public void setOnTaskUpdatedListener(OnTaskUpdatedListener listener) {
+ this.mOnTaskUpdatedListener = listener;
+ }
+
+ public ArrayList getAllTasks() {
+ return new ArrayList(mTaskList);
+ }
+
+ public void clearData() {
+ mTaskList.clear();
+ if (mTaskAdapter != null && mTaskAdapter.mData != null) {
+ mTaskAdapter.mData.clear();
+ }
+ mTaskAdapter.notifyDataSetChanged();
+ mBindPositionId = null;
+ }
+
+ public void triggerTaskSync() {
+ if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
+ mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mTaskList));
+ }
+ }
+
+ // ---------------------- 内部工具方法 ----------------------
+ private boolean isTaskMatchedWithPosition(PositionTaskModel task) {
+ if (task == null || mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
+ return false;
+ }
+ return mBindPositionId.equals(task.getPositionId());
+ }
+
+ // ---------------------- 内部Adapter:适配 isBingo 红点(核心调整) ----------------------
+ private class TaskListAdapter extends RecyclerView.Adapter {
+ private final List mData;
+
+ public TaskListAdapter(List data) {
+ this.mData = data;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.isEmpty() ? 1 : mData.size();
+ }
+
+ // 调整:根据“是否空列表”+“视图模式”区分视图类型(确保简单/编辑模式加载对应布局)
+ @Override
+ public int getItemViewType(int position) {
+ if (mData.isEmpty()) {
+ return 0; // 0=空提示
+ } else {
+ return mCurrentViewMode; // 1=简单模式,2=编辑模式(复用视图模式常量)
+ }
+ }
+
+ // 调整:按视图类型加载布局(简单模式加载带红点的布局,编辑模式加载原有布局)
+ @NonNull
+ @Override
+ public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ Context context = parent.getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+
+ if (viewType == 0) {
+ // 空提示布局
+ View emptyView = inflater.inflate(R.layout.item_task_empty, parent, false);
+ return new EmptyViewHolder(emptyView);
+ } else if (viewType == VIEW_MODE_SIMPLE) {
+ // 简单模式布局(带 isBingo 红点)
+ View simpleTaskView = inflater.inflate(R.layout.item_position_task_simple, parent, false);
+ return new SimpleTaskViewHolder(simpleTaskView);
+ } else {
+ // 编辑模式布局(原有布局不变)
+ View editTaskView = inflater.inflate(R.layout.item_task_content, parent, false);
+ return new TaskContentViewHolder(editTaskView);
+ }
+ }
+
+ // 调整:按视图类型绑定数据(简单模式绑定红点+文本,编辑模式绑定原有逻辑)
+ @Override
+ public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
+ // 空提示处理(不变)
+ if (holder instanceof EmptyViewHolder) {
+ EmptyViewHolder emptyHolder = (EmptyViewHolder) holder;
+ TextView tvEmptyTip = (TextView) emptyHolder.itemView.findViewById(R.id.tv_task_empty_tip);
+ tvEmptyTip.setText(mCurrentViewMode == VIEW_MODE_EDIT ? "暂无任务,点击\"添加新任务\"创建" : "暂无启用的任务");
+ return;
+ }
+
+ // 任务项有效性校验(不变)
+ if (position >= mData.size()) {
+ return;
+ }
+ final PositionTaskModel task = mData.get(position);
+ if (task == null) {
+ return;
+ }
+
+ // 简单模式:绑定红点(isBingo)和文本数据
+ if (holder instanceof SimpleTaskViewHolder) {
+ SimpleTaskViewHolder simpleHolder = (SimpleTaskViewHolder) holder;
+ // 绑定任务描述
+ simpleHolder.tvSimpleTaskDesc.setText(String.format("任务:%s", task.getTaskDescription()));
+ // 绑定距离条件
+ String distanceCond = task.isGreaterThan() ? "大于" : "小于";
+ simpleHolder.tvSimpleDistanceCond.setText(String.format("条件:距离 %s %d 米", distanceCond, task.getDiscussDistance()));
+ // 绑定启用状态
+ simpleHolder.tvSimpleIsEnable.setText(task.isEnable() ? "状态:已启用" : "状态:已禁用");
+ // 核心:根据 isBingo 控制红点显示(true=显示,false=隐藏)
+ simpleHolder.vBingoDot.setVisibility(task.isBingo() ? View.VISIBLE : View.GONE);
+ }
+ // 编辑模式:沿用原有绑定逻辑(核心修复在此处)
+ else if (holder instanceof TaskContentViewHolder) {
+ TaskContentViewHolder contentHolder = (TaskContentViewHolder) holder;
+ bindTaskData(contentHolder, task, position);
+ }
+ }
+
+ // ---------------------- 核心修复:编辑模式绑定逻辑(解决布局中通知异常) ----------------------
+ private void bindTaskData(final TaskContentViewHolder holder, final PositionTaskModel task, final int position) {
+ String taskDesc = (task.getTaskDescription() == null) ? "未设置描述" : task.getTaskDescription();
+ holder.tvTaskDesc.setText(String.format("任务:%s", taskDesc));
+
+ String distanceCondition = task.isGreaterThan() ? "大于" : "小于";
+ holder.tvTaskDistance.setText(String.format("条件:%s %d 米", distanceCondition, task.getDiscussDistance()));
+
+ // 修复点1:先移除开关监听,再设置状态(避免设值时触发回调导致异常)
+ holder.cbTaskEnable.setOnCheckedChangeListener(null);
+ holder.cbTaskEnable.setChecked(task.isEnable());
+ holder.cbTaskEnable.setEnabled(mCurrentViewMode == VIEW_MODE_EDIT);
+
+ if (mCurrentViewMode == VIEW_MODE_EDIT) {
+ holder.btnEditTask.setVisibility(View.VISIBLE);
+ holder.btnDeleteTask.setVisibility(View.VISIBLE);
+
+ // 删除按钮逻辑(不变,本身在点击时执行,不涉及布局中通知)
+ holder.btnDeleteTask.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mData.remove(position);
+ notifyItemRemoved(position);
+ notifyItemRangeChanged(position, mData.size());
+ if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
+ mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData));
+ }
+ Toast.makeText(getContext(), "任务已删除", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ // 编辑按钮逻辑(不变,弹窗保存时修复通知时机)
+ holder.btnEditTask.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showTaskEditDialog(task, position);
+ }
+ });
+
+ // 修复点2:开关监听-用 RecyclerView.post 延迟执行 notify(避免布局中调用)
+ holder.cbTaskEnable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ task.setIsEnable(isChecked); // 先更新数据源(必须执行)
+ // 关键:通过 mRvTasks.post 延迟通知,确保在布局计算/滚动结束后执行
+ mRvTasks.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyItemChanged(position);
+ if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
+ mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData));
+ }
+ }
+ });
+ }
+ });
+ } else {
+ holder.btnEditTask.setVisibility(View.GONE);
+ holder.btnDeleteTask.setVisibility(View.GONE);
+ holder.cbTaskEnable.setOnCheckedChangeListener(null);
+ }
+ }
+
+ // ---------------------- 修复:编辑弹窗保存逻辑(延迟通知,避免极端场景异常) ----------------------
+ private void showTaskEditDialog(final PositionTaskModel task, final int position) {
+ final Context context = getContext();
+ View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_edit_task, null);
+
+ final EditText etEditDesc = (EditText) dialogView.findViewById(R.id.et_edit_task_desc);
+ final RadioGroup rgDistanceCondition = (RadioGroup) dialogView.findViewById(R.id.rg_distance_condition);
+ final EditText etEditDistance = (EditText) dialogView.findViewById(R.id.et_edit_distance);
+ Button btnCancel = (Button) dialogView.findViewById(R.id.btn_dialog_cancel);
+ Button btnSave = (Button) dialogView.findViewById(R.id.btn_dialog_save);
+
+ etEditDesc.setText(task.getTaskDescription());
+ etEditDesc.setSelection(etEditDesc.getText().length());
+
+ if (task.isGreaterThan()) {
+ rgDistanceCondition.check(R.id.rb_greater_than);
+ } else {
+ rgDistanceCondition.check(R.id.rb_less_than);
+ }
+
+ etEditDistance.setText(String.valueOf(task.getDiscussDistance()));
+
+ final android.app.AlertDialog dialog = new android.app.AlertDialog.Builder(context)
+ .setView(dialogView)
+ .create();
+ dialog.show();
+
+ btnCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ btnSave.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String newDesc = etEditDesc.getText().toString().trim();
+ String distanceStr = etEditDistance.getText().toString().trim();
+
+ if (distanceStr.isEmpty()) {
+ Toast.makeText(context, "请输入有效距离", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ int newDistance;
+ try {
+ newDistance = Integer.parseInt(distanceStr);
+ if (newDistance < 1) {
+ Toast.makeText(context, "距离不能小于1米", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (NumberFormatException e) {
+ Toast.makeText(context, "距离请输入数字", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ task.setTaskDescription(newDesc);
+ task.setDiscussDistance(newDistance);
+ boolean isGreater = rgDistanceCondition.getCheckedRadioButtonId() == R.id.rb_greater_than;
+ task.setIsGreaterThan(isGreater);
+ task.setPositionId(mBindPositionId);
+
+ // 修复点3:弹窗保存后延迟通知(同开关逻辑,避免列表滚动时异常)
+ mRvTasks.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyItemChanged(position);
+ if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
+ mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList(mData));
+ }
+ }
+ });
+
+ dialog.dismiss();
+ Toast.makeText(context, "任务已更新", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ // ---------------------- ViewHolder 定义(完全不变) ----------------------
+ // 基础抽象 ViewHolder(不变)
+ public abstract class TaskViewHolder extends RecyclerView.ViewHolder {
+ public TaskViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ }
+
+ // 空提示 Holder(不变)
+ public class EmptyViewHolder extends TaskViewHolder {
+ public EmptyViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ }
+
+ // 新增:简单模式 Holder(绑定带红点的布局控件)
+ public class SimpleTaskViewHolder extends TaskViewHolder {
+ TextView tvSimpleTaskDesc; // 任务描述
+ TextView tvSimpleDistanceCond;// 距离条件
+ TextView tvSimpleIsEnable; // 启用状态
+ View vBingoDot; // isBingo 红点控件
+
+ public SimpleTaskViewHolder(@NonNull View itemView) {
+ super(itemView);
+ // 绑定简单模式布局中的控件(与 item_task_simple.xml 完全对应)
+ tvSimpleTaskDesc = itemView.findViewById(R.id.tv_simple_task_desc);
+ tvSimpleDistanceCond = itemView.findViewById(R.id.tv_simple_distance_cond);
+ tvSimpleIsEnable = itemView.findViewById(R.id.tv_simple_is_enable);
+ vBingoDot = itemView.findViewById(R.id.v_bingo_dot);
+ }
+ }
+
+ // 编辑模式 Holder(原有逻辑,完全不变)
+ public class TaskContentViewHolder extends TaskViewHolder {
+ TextView tvTaskDesc;
+ TextView tvTaskDistance;
+ CompoundButton cbTaskEnable;
+ Button btnEditTask;
+ Button btnDeleteTask;
+
+ public TaskContentViewHolder(@NonNull View itemView) {
+ super(itemView);
+ tvTaskDesc = (TextView) itemView.findViewById(R.id.tv_task_desc);
+ tvTaskDistance = (TextView) itemView.findViewById(R.id.tv_task_distance);
+ cbTaskEnable = (CompoundButton) itemView.findViewById(R.id.cb_task_enable);
+ btnEditTask = (Button) itemView.findViewById(R.id.btn_edit_task);
+ btnDeleteTask = (Button) itemView.findViewById(R.id.btn_delete_task);
+ }
+ }
+ }
+}
+
diff --git a/positions/src/main/res/drawable-v24/ic_launcher_foreground.xml b/positions/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..c7bd21db
--- /dev/null
+++ b/positions/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/bg_bingo_dot.xml b/positions/src/main/res/drawable/bg_bingo_dot.xml
new file mode 100644
index 00000000..248bd1b7
--- /dev/null
+++ b/positions/src/main/res/drawable/bg_bingo_dot.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/bg_task_item.xml b/positions/src/main/res/drawable/bg_task_item.xml
new file mode 100644
index 00000000..d8a68b89
--- /dev/null
+++ b/positions/src/main/res/drawable/bg_task_item.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/btn_cancel_bg.xml b/positions/src/main/res/drawable/btn_cancel_bg.xml
new file mode 100644
index 00000000..74ca496b
--- /dev/null
+++ b/positions/src/main/res/drawable/btn_cancel_bg.xml
@@ -0,0 +1,18 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/btn_confirm_bg.xml b/positions/src/main/res/drawable/btn_confirm_bg.xml
new file mode 100644
index 00000000..9524b333
--- /dev/null
+++ b/positions/src/main/res/drawable/btn_confirm_bg.xml
@@ -0,0 +1,18 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/btn_delete_bg.xml b/positions/src/main/res/drawable/btn_delete_bg.xml
new file mode 100644
index 00000000..c7f86237
--- /dev/null
+++ b/positions/src/main/res/drawable/btn_delete_bg.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/circle_button_bg.xml b/positions/src/main/res/drawable/circle_button_bg.xml
new file mode 100644
index 00000000..5ed6038a
--- /dev/null
+++ b/positions/src/main/res/drawable/circle_button_bg.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/edittext_bg.xml b/positions/src/main/res/drawable/edittext_bg.xml
new file mode 100644
index 00000000..85b3d284
--- /dev/null
+++ b/positions/src/main/res/drawable/edittext_bg.xml
@@ -0,0 +1,18 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/ic_launcher.xml b/positions/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 00000000..1fd5efd2
--- /dev/null
+++ b/positions/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/positions/src/main/res/drawable/ic_launcher_background.xml b/positions/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..d5fccc53
--- /dev/null
+++ b/positions/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/ic_launcher_beta.xml b/positions/src/main/res/drawable/ic_launcher_beta.xml
new file mode 100644
index 00000000..bc3dca63
--- /dev/null
+++ b/positions/src/main/res/drawable/ic_launcher_beta.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/ic_positions.png b/positions/src/main/res/drawable/ic_positions.png
new file mode 100644
index 00000000..68ea75b5
Binary files /dev/null and b/positions/src/main/res/drawable/ic_positions.png differ
diff --git a/positions/src/main/res/drawable/item_bg_edit.xml b/positions/src/main/res/drawable/item_bg_edit.xml
new file mode 100644
index 00000000..589db77d
--- /dev/null
+++ b/positions/src/main/res/drawable/item_bg_edit.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/item_bg_simple.xml b/positions/src/main/res/drawable/item_bg_simple.xml
new file mode 100644
index 00000000..16170548
--- /dev/null
+++ b/positions/src/main/res/drawable/item_bg_simple.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/drawable/item_position_bg.xml b/positions/src/main/res/drawable/item_position_bg.xml
new file mode 100644
index 00000000..a6dba446
--- /dev/null
+++ b/positions/src/main/res/drawable/item_position_bg.xml
@@ -0,0 +1,18 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/activity_location.xml b/positions/src/main/res/layout/activity_location.xml
new file mode 100644
index 00000000..e751f33d
--- /dev/null
+++ b/positions/src/main/res/layout/activity_location.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/activity_main.xml b/positions/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..67f7f967
--- /dev/null
+++ b/positions/src/main/res/layout/activity_main.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/dialog_edit_position.xml b/positions/src/main/res/layout/dialog_edit_position.xml
new file mode 100644
index 00000000..c9245e58
--- /dev/null
+++ b/positions/src/main/res/layout/dialog_edit_position.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/dialog_edit_task.xml b/positions/src/main/res/layout/dialog_edit_task.xml
new file mode 100644
index 00000000..a282b533
--- /dev/null
+++ b/positions/src/main/res/layout/dialog_edit_task.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_position_edit.xml b/positions/src/main/res/layout/item_position_edit.xml
new file mode 100644
index 00000000..f1d8980b
--- /dev/null
+++ b/positions/src/main/res/layout/item_position_edit.xml
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_position_empty.xml b/positions/src/main/res/layout/item_position_empty.xml
new file mode 100644
index 00000000..37ab0e4e
--- /dev/null
+++ b/positions/src/main/res/layout/item_position_empty.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_position_simple.xml b/positions/src/main/res/layout/item_position_simple.xml
new file mode 100644
index 00000000..ec4e69fa
--- /dev/null
+++ b/positions/src/main/res/layout/item_position_simple.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_position_task_edit.xml b/positions/src/main/res/layout/item_position_task_edit.xml
new file mode 100644
index 00000000..1648dac3
--- /dev/null
+++ b/positions/src/main/res/layout/item_position_task_edit.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_position_task_simple.xml b/positions/src/main/res/layout/item_position_task_simple.xml
new file mode 100644
index 00000000..5e284bb0
--- /dev/null
+++ b/positions/src/main/res/layout/item_position_task_simple.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_task_content.xml b/positions/src/main/res/layout/item_task_content.xml
new file mode 100644
index 00000000..aa3e1a34
--- /dev/null
+++ b/positions/src/main/res/layout/item_task_content.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/item_task_empty.xml b/positions/src/main/res/layout/item_task_empty.xml
new file mode 100644
index 00000000..a676c6a3
--- /dev/null
+++ b/positions/src/main/res/layout/item_task_empty.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/layout/view_position_task_list.xml b/positions/src/main/res/layout/view_position_task_list.xml
new file mode 100644
index 00000000..7cac325b
--- /dev/null
+++ b/positions/src/main/res/layout/view_position_task_list.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/positions/src/main/res/menu/menu_item_edit.xml b/positions/src/main/res/menu/menu_item_edit.xml
new file mode 100644
index 00000000..b37482b1
--- /dev/null
+++ b/positions/src/main/res/menu/menu_item_edit.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/positions/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/positions/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/positions/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/positions/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/positions/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/positions/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/positions/src/main/res/mipmap-hdpi/ic_launcher.png b/positions/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a2f59082
Binary files /dev/null and b/positions/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/positions/src/main/res/mipmap-hdpi/ic_launcher_round.png b/positions/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1b523998
Binary files /dev/null and b/positions/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/positions/src/main/res/mipmap-mdpi/ic_launcher.png b/positions/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..ff10afd6
Binary files /dev/null and b/positions/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/positions/src/main/res/mipmap-mdpi/ic_launcher_round.png b/positions/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..115a4c76
Binary files /dev/null and b/positions/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/positions/src/main/res/mipmap-xhdpi/ic_launcher.png b/positions/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..dcd3cd80
Binary files /dev/null and b/positions/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/positions/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/positions/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..459ca609
Binary files /dev/null and b/positions/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/positions/src/main/res/mipmap-xxhdpi/ic_launcher.png b/positions/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..8ca12fe0
Binary files /dev/null and b/positions/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/positions/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/positions/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..8e19b410
Binary files /dev/null and b/positions/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/positions/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/positions/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b824ebdd
Binary files /dev/null and b/positions/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/positions/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/positions/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..4c19a13c
Binary files /dev/null and b/positions/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/positions/src/main/res/values-zh/strings.xml b/positions/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000..5e97cc82
--- /dev/null
+++ b/positions/src/main/res/values-zh/strings.xml
@@ -0,0 +1,4 @@
+
+
+ 寻龙记
+
diff --git a/positions/src/main/res/values/colors.xml b/positions/src/main/res/values/colors.xml
new file mode 100644
index 00000000..97f31430
--- /dev/null
+++ b/positions/src/main/res/values/colors.xml
@@ -0,0 +1,33 @@
+
+
+ #009688
+ #00796B
+ #FF9800
+
+ #2E8B57
+
+
+ #999999
+
+
+ #F5F5F5
+ #EEEEEE
+ #E0E0E0
+ #F44336
+
+
+ #9E9E9E
+ #616161
+
+
+ #FFFFFF
+ #000000
+ #2196F3
+ #F44336
+ #F5F5F5
+
+
+ #4CAF50
+ #FFC107
+
+
diff --git a/positions/src/main/res/values/strings.xml b/positions/src/main/res/values/strings.xml
new file mode 100644
index 00000000..495482a7
--- /dev/null
+++ b/positions/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Positions
+
diff --git a/positions/src/main/res/values/styles.xml b/positions/src/main/res/values/styles.xml
new file mode 100644
index 00000000..a70e242f
--- /dev/null
+++ b/positions/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/positions/src/stage/AndroidManifest.xml b/positions/src/stage/AndroidManifest.xml
new file mode 100644
index 00000000..ee78d9fa
--- /dev/null
+++ b/positions/src/stage/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positions/src/stage/res/values/strings.xml b/positions/src/stage/res/values/strings.xml
new file mode 100644
index 00000000..ace0c418
--- /dev/null
+++ b/positions/src/stage/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/settings.gradle-demo b/settings.gradle-demo
index c2084594..cf7adb44 100644
--- a/settings.gradle-demo
+++ b/settings.gradle-demo
@@ -64,4 +64,8 @@
// WebPageSources 项目编译设置
//include ':webpagesources'
-//rootProject.name = "webpagesources"
\ No newline at end of file
+//rootProject.name = "webpagesources"
+
+// Positions 项目编译设置
+//include ':positions'
+//rootProject.name = "positions"