Compare commits

..

13 Commits
aes ... gitsion

Author SHA1 Message Date
094239edfa OpenCode崩溃现场源码存档 2026-06-04 20:31:51 +08:00
15727b8d66 调试模式下主窗口工具栏添加"应用日志"菜单入口
- menu_main.xml 新增 group_debug 组,内含"应用日志"菜单项(默认隐藏)
- MainActivity.onCreateOptionsMenu 判断 GlobalApplication.isDebugging(),调试状态下显示该组
- MainActivity.onOptionsItemSelected 处理菜单点击,调用 LogActivity.startLogActivity(this, false) 打开日志窗口
2026-06-04 18:26:28 +08:00
c08bbf7b74 移除LogView控件并修复AboutView在Android 16上的Inflate崩溃
- 移除主窗口 activity_main.xml 底部 LogView 面板,清理 MainActivity.java 中相关字段与 onResume 调用,修复 NPE 崩溃
- MyAppTheme 补充 aboutViewBackgroundColor/aboutViewTextColor/aboutViewTitleColor/aboutViewDividerColor 属性定义,修复 AboutActivity 在 Android 16 上的 UnsupportedOperationException 崩溃
2026-06-04 18:03:50 +08:00
b2d2d7ac99 修复GPS订阅数据流断裂,打通MainService→广播→ChildService完整链路
- GpsSubscribeResult 增加 latitude/longitude/locationTime 字段及序列化
- GpsSubscribeReceiverService 重写 onCreate/onDestroy 动态注册广播接收器,onStartCommand 读取 SID 并绑定
- GpsSubscribeControlView 启动服务时传递 EXTRA_SUBSCRIBE_SID
- MainService 步长判断通过后调用 sendSubscribeResult();initManager() 补调 initContext(this)
- GpsReceiverChildService{1,2,3} 补调 super.onStartCommand
- AndroidManifest.xml 修正广播 Action 为 cc.winboll.studio.GPS_SUBSCRIBE_CALLBACK
- GpsSubscribeManager/GpsSubscribeObserverReceiver 广播 Extra 键名改用常量
- GpsSubscribeConst 新增 EXTRA_SUBSCRIBE_SID/EXTRA_SUBSCRIBE_RESULT/EXTRA_LOCATION_POINT
2026-06-04 17:51:13 +08:00
b9bc7dfa18 修复Gitsion项目在Android 16上的布局Inflate崩溃
- 修正 activity_main.xml 中 GpsSubscribeControlView 的包名
  (libgpsrelaysentinel → libgitsion)
- 补充 MyAppTheme 缺失的自定义主题属性,新增 DebugActivityTheme
  解决 view_log.xml 中 ?attr/colorTextBackgound 等引用无法
  在 Android 16 (SDK 36) 上解析导致的 InflateException
- 添加 colors.xml 中对应的主题颜色定义
2026-06-04 17:10:14 +08:00
2f4977b264 修正说明书 2026-06-03 20:45:11 +08:00
9db9a4cbd3 添加Gitsion项目 2026-06-03 20:40:02 +08:00
qinglong
bdd8d1e2b6 合并模块APPBase 同步最新时间标签appbase-v15.20.25 2026-06-03 07:00:01 +08:00
qinglong
138ca80e35 合并模块APPBase 同步最新时间标签appbase-v15.20.23 2026-06-03 06:06:41 +08:00
qinglong
7a3b54b78b 合并模块WinBoLL 同步最新时间标签winboll-v15.20.6 2026-06-02 21:00:01 +08:00
qinglong
db804d1897 合并模块AES 同步最新时间标签aes-v15.20.12 2026-06-02 08:55:05 +08:00
qinglong
039c8fcd98 合并模块WinBoLL 同步最新时间标签winboll-v15.20.5 2026-06-02 04:00:01 +08:00
qinglong
ae63d1ec0a 合并模块AES 同步最新时间标签aes-v15.20.11 2026-06-02 03:00:01 +08:00
249 changed files with 17387 additions and 131 deletions

4
.gitignore vendored
View File

@@ -97,5 +97,5 @@ lint-results.html
## WinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
#/settings.gradle
#/gradle.properties
/settings.gradle
/gradle.properties

View File

@@ -1 +0,0 @@
15.20

View File

@@ -6,11 +6,11 @@
## 核心声明
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
本文档**唯一核心设计目的**通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
@@ -24,7 +24,7 @@
### 1. 公共开源分支唯一标识
**文件名: LICENSE**
**文件名LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
@@ -34,7 +34,7 @@
### 2. 私有开发分支唯一标识
**文件名: LICENSE-Private**
**文件名LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
@@ -44,15 +44,15 @@
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
1. **公共主流分支**固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
2. **私有开发分支**统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 私有分支 → 公共分支**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
- 公共分支 → 私有分支允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
@@ -68,7 +68,7 @@
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
@@ -84,14 +84,14 @@
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
4. 分支属性校验以根目录授权文件为唯一标准只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规公共分支仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
- 合规私有分支仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
- 异常状态两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注: 文档部分内容可能由 AI 生成)
> (注文档部分内容可能由 AI 生成)

View File

@@ -6,11 +6,11 @@
## 核心声明
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
本文档**唯一核心设计目的**通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
@@ -24,7 +24,7 @@
### 1. 公共开源分支唯一标识
**文件名: LICENSE**
**文件名LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
@@ -34,7 +34,7 @@
### 2. 私有开发分支唯一标识
**文件名: LICENSE-Private**
**文件名LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
@@ -44,15 +44,15 @@
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
1. **公共主流分支**固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
2. **私有开发分支**统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 私有分支 → 公共分支**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
- 公共分支 → 私有分支允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
@@ -68,7 +68,7 @@
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
@@ -84,14 +84,14 @@
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
4. 分支属性校验以根目录授权文件为唯一标准只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规公共分支仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
- 合规私有分支仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
- 异常状态两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注: 文档部分内容可能由 AI 生成)
> (注文档部分内容可能由 AI 生成)

View File

@@ -39,6 +39,10 @@ android {
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Jun 29 11:29:30 HKT 2026
stageCount=18
#Tue Jun 02 08:54:20 HKT 2026
stageCount=13
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.17
publishVersion=15.20.12
buildCount=0
baseBetaVersion=15.20.18
baseBetaVersion=15.20.13

View File

@@ -65,7 +65,7 @@ public class AboutActivity extends BaseWinBoLLActivity {
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitName("AES");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);

View File

@@ -7,12 +7,9 @@ package cc.winboll.studio.aes;
*/
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
public class App extends GlobalApplication {
@@ -21,25 +18,12 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
try {
super.onCreate();
ToastUtils.init(this);
WinBoLLActivityManager.init(this);
AESThemeUtil.init(null);
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
super.onCreate();
AESThemeUtil.init(null);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override

View File

@@ -1,7 +1,11 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.winboll.studio.libaes.views.ADsControlView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -16,7 +20,7 @@ public class SettingsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
//ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override

View File

@@ -6,5 +6,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed May 27 14:51:29 HKT 2026
stageCount=23
#Wed Jun 03 06:52:38 HKT 2026
stageCount=26
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.22
publishVersion=15.20.25
buildCount=0
baseBetaVersion=15.20.23
baseBetaVersion=15.20.26

View File

@@ -1,8 +1,11 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,10 +24,24 @@ public class App extends GlobalApplication {
*/
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
try {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashHandler.CrashActivity.class
);
}
}
/**

239
gitsion/README.md Normal file
View File

@@ -0,0 +1,239 @@
# Gitsion
---
## 中文文档
### 项目介绍
Gitsion 是一款专业的 **GPS 定位中继守护工具**,支持真实系统 GPS 定位监听与模拟 GPS 坐标仿真双模式运行。应用后台常驻前台服务,实时接收系统 GPS 位置数据,内置订阅者步长阈值判断机制,可对多个 GPS 订阅视图进行定点推送管理。
#### 核心功能
- **双模式运行**:支持真实 GPS 工作模式与虚拟仿真模式一键切换
- **前台常驻服务**`MainService` 作为前台 Service 持续监听 GPS 定位变化
- **订阅者管理**:内置 `GpsSubscribeManager``SubscribeLocationManager`,支持多订阅者步长阈值推送
- **模拟控制面板**:支持八大方位选择、自定义移动距离,自动计算偏移目标经纬度
- **实时日志输出**:集成 `LogView` 面板,方便调试定位轨迹与订阅推送状态
- **崩溃处理**`App` 类提供全局 CrashHandler 与 CrashActivity 展示崩溃日志
- **关于页面**:工具栏提供 About 按钮,可查看应用版本与项目信息
#### 技术栈
| 项目 | 版本/说明 |
|------|-----------|
| 编程语言 | Java 7源码 |
| 编译环境 | Java 11Gradle 编译) |
| Gradle 插件 | 7.2.1 |
| 最低 API | API 26 (Android 8.0) |
| 目标 API | API 30 (Android 11) |
| 编译 API | API 30 |
#### 模块结构
本项目采用多模块 Gradle 结构:
| 模块 | 类型 | 说明 |
|------|------|------|
| `:gitsion` | application | 主应用模块MainActivity、MainService、AboutActivity 等) |
| `:libgitsion` | library | GPS 中继核心类库GpsSubscribeManager、SubscribeLocationManager 等) |
#### 核心依赖库
**网络相关**
- OkHttp 4.4.1 / 3.14.9 — HTTP 客户端
- Gson 2.10.1 — JSON 解析
**终端模拟**
- Termux: terminal-emulator 0.118.0
- Termux: terminal-view 0.118.0
- Termux: termux-shared 0.118.0
**功能组件**
- ZXing 3.4.1 — 二维码生成与扫描
- JSch 0.1.55 — SSH/SFTP 客户端
- Jsoup 1.13.1 — HTML 解析
- FastJSON 1.2.76 — JSON 处理
**UI 组件**
- Material Design 1.4.0
- AndroidX 组件库
- PullRefreshLayout 1.2.0 — 下拉刷新
#### 编译说明
**调试版编译**
```bash
./gradlew assembleBetaDebug
```
**阶段版编译(发布)**
```bash
bash .winboll/bashPublishAPKAddTag.sh gitsion
```
**版本管理**
版本信息由 `gitsion/build.properties` 管理:
- `baseVersion` — 基础版本号
- `stageCount` — 阶段构建次数
- `publishVersion` — 发布版本号
- `buildCount` — 构建次数
#### 权限说明
应用需要以下权限:
- `ACCESS_FINE_LOCATION` — 精确定位
- `ACCESS_COARSE_LOCATION` — 大致定位
- `ACCESS_BACKGROUND_LOCATION` — 后台定位
- `FOREGROUND_SERVICE` — 前台服务
#### 项目结构
```
gitsion/
├── src/main/
│ ├── java/cc/winboll/studio/gitsion/
│ │ ├── App.java # Application 类,初始化与崩溃处理
│ │ ├── MainActivity.java # 主控制页面GPS服务开关、模拟面板、订阅视图
│ │ ├── MainService.java # GPS 定位核心前台服务
│ │ ├── AboutActivity.java # 关于页面
│ │ └── GpsReceiverChildService[1-3].java # GPS 接收子服务
│ ├── res/
│ │ ├── layout/ # 布局文件
│ │ ├── menu/ # 菜单文件
│ │ └── values/ # 资源值文件
│ ├── libs/ # 本地库文件
│ └── AndroidManifest.xml # 应用清单
├── build.gradle # 模块构建配置
└── build.properties # 版本配置文件
```
#### 参与贡献
1. Fork 本仓库
2. 新建功能分支 (`git checkout -b feat_xxx`)
3. 提交代码(作者: ZhanGSKen <ZhanGSKen@QQ.COM>
4. 新建 Pull Request
#### 许可证
[待添加许可证信息]
---
## English Documentation
### Project Introduction
gitsion is a professional **GPS relay and guardian tool**, supporting dual modes of real system GPS location monitoring and simulated GPS coordinate simulation. It runs as a foreground persistent background service, receives real-time system GPS location data, and builds-in subscriber step threshold judgment mechanism to manage fixed-point push for multiple GPS subscription views.
#### Core Features
- **Dual Mode Operation**: One-click switch between real GPS working mode and virtual simulation mode
- **Foreground Persistent Service**: `MainService` as a foreground Service continuously monitors GPS location changes
- **Subscriber Management**: Built-in `GpsSubscribeManager` and `SubscribeLocationManager`, supporting multi-subscriber step threshold push
- **Simulation Control Panel**: Supports eight direction selections, custom moving distance, and automatic offset target coordinate calculation
- **Real-time Log Output**: Integrated `LogView` panel for debugging location tracks and subscription push status
- **Crash Handling**: `App` class provides global CrashHandler and CrashActivity for crash log display
- **About Page**: Toolbar provides an About button to view app version and project information
#### Tech Stack
| Item | Version/Description |
|------|---------------------|
| Programming Language | Java 7 (source code) |
| Build Environment | Java 11 (Gradle compilation) |
| Gradle Plugin | 7.2.1 |
| Minimum API | API 26 (Android 8.0) |
| Target API | API 30 (Android 11) |
| Compile API | API 30 |
#### Module Structure
This project uses a multi-module Gradle structure:
| Module | Type | Description |
|--------|------|-------------|
| `:gitsion` | application | Main application module (MainActivity, MainService, AboutActivity, etc.) |
| `:libgitsion` | library | GPS relay core library (GpsSubscribeManager, SubscribeLocationManager, etc.) |
#### Core Dependencies
**Networking**
- OkHttp 4.4.1 / 3.14.9 — HTTP client
- Gson 2.10.1 — JSON parsing
**Terminal Emulation**
- Termux: terminal-emulator 0.118.0
- Termux: terminal-view 0.118.0
- Termux: termux-shared 0.118.0
**Functional Components**
- ZXing 3.4.1 — QR code generation and scanning
- JSch 0.1.55 — SSH/SFTP client
- Jsoup 1.13.1 — HTML parsing
- FastJSON 1.2.76 — JSON processing
**UI Components**
- Material Design 1.4.0
- AndroidX libraries
- PullRefreshLayout 1.2.0 — Pull-to-refresh
#### Build Instructions
**Debug Build**
```bash
./gradlew assembleBetaDebug
```
**Stage Build (Release)**
```bash
bash .winboll/bashPublishAPKAddTag.sh gitsion
```
**Version Management**
Version info is managed by `gitsion/build.properties`:
- `baseVersion` — Base version number
- `stageCount` — Stage build count
- `publishVersion` — Release version number
- `buildCount` — Build count
#### Permissions
The app requires the following permissions:
- `ACCESS_FINE_LOCATION` — Precise location
- `ACCESS_COARSE_LOCATION` — Approximate location
- `ACCESS_BACKGROUND_LOCATION` — Background location
- `FOREGROUND_SERVICE` — Foreground service
#### Project Structure
```
gitsion/
├── src/main/
│ ├── java/cc/winboll/studio/gitsion/
│ │ ├── App.java # Application class, initialization and crash handling
│ │ ├── MainActivity.java # Main control page (GPS service switch, simulation panel, subscription views)
│ │ ├── MainService.java # GPS location core foreground service
│ │ ├── AboutActivity.java # About page
│ │ └── GpsReceiverChildService[1-3].java # GPS receiver child services
│ ├── res/
│ │ ├── layout/ # Layout files
│ │ ├── menu/ # Menu files
│ │ └── values/ # Resource value files
│ ├── libs/ # Local library files
│ └── AndroidManifest.xml # App manifest
├── build.gradle # Module build configuration
└── build.properties # Version configuration file
```
#### Contributing
1. Fork this repository
2. Create a feature branch (`git checkout -b feat_xxx`)
3. Commit your changes (Author: ZhanGSKen <ZhanGSKen@QQ.COM>)
4. Create a Pull Request
#### License
[License information to be added]

View File

@@ -0,0 +1 @@

122
gitsion/build.gradle Normal file
View File

@@ -0,0 +1,122 @@
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 {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.gitsion"
minSdkVersion 26
// 适配MIUI12
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
}
}
// 确保 Java 7 兼容性
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
api project(':libgitsion')
api 'com.google.code.gson:gson:2.10.1'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// 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.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// 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 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.20.15'
api 'cc.winboll.studio:libappbase:15.20.26'
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
gitsion/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Thu Jun 04 19:50:05 HKT 2026
stageCount=27
libraryProject=
baseVersion=15.11
publishVersion=15.11.26
buildCount=53
baseBetaVersion=15.11.27

137
gitsion/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,137 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件避免资源ID混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法避免JNI调用失败
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1米盟广告请求依赖完善Lambda兼容
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Gitsion★</string>
<string name="app_description">一款支持真实/模拟定位的GPS中继工具可后台常驻实现位置数据转发、调试与仿真适配开发测试使用。</string>
</resources>

View File

@@ -0,0 +1,63 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.gitsion">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 在后台使用位置信息 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
<service
android:name=".MainService"
android:enabled="true"
android:exported="false"/>
<service android:name=".GpsReceiverChildService1"/>
<service android:name=".GpsReceiverChildService2"/>
<service android:name=".GpsReceiverChildService3"/>
<activity android:name=".AboutActivity"/>
<activity android:name=".GpsHistoryActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,58 @@
package cc.winboll.studio.gitsion;
import android.app.Activity;
import android.os.Bundle;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 15:39
*/
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
public class AboutActivity extends AppCompatActivity {
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "gpsrelaysentinel";
APPInfo appInfo = new APPInfo();
appInfo.setAppName("GPSRelaySentinel");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=GPSRelaySentinel");
appInfo.setAppAPKName("GPSRelaySentinel");
appInfo.setAppAPKFolderName("GPSRelaySentinel");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}

View File

@@ -0,0 +1,357 @@
package cc.winboll.studio.gitsion;
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.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.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
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.io.PrintWriter;
import java.io.StringWriter;
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;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
try {
super.onCreate();
ToastUtils.init(this);
WinBoLLActivityManager.init(this);
AESThemeUtil.init(null);
GpsHistoryManager.getInstance().init(this);
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
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<String, String> head = new LinkedHashMap<String, String>();
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();
}
}
}

View File

@@ -0,0 +1,151 @@
package cc.winboll.studio.gitsion;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import cc.winboll.studio.gitsion.db.GpsHistoryDatabaseHelper;
import cc.winboll.studio.gitsion.model.GpsHistoryRecord;
public class GpsHistoryActivity extends Activity {
private ListView mListView;
private TextView mEmptyHint;
private TextView mTvSystemCount;
private TextView mTvSimCount;
private GpsHistoryAdapter mAdapter;
private final Runnable mRefreshRunnable = new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
refreshData();
}
});
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gps_history);
mListView = findViewById(R.id.lv_gps_history);
mEmptyHint = findViewById(R.id.tv_empty_hint);
mTvSystemCount = findViewById(R.id.tv_system_count);
mTvSimCount = findViewById(R.id.tv_sim_count);
List<GpsHistoryRecord> records = GpsHistoryManager.getInstance().getRecords();
mAdapter = new GpsHistoryAdapter(records);
mListView.setAdapter(mAdapter);
updateEmptyHint(records.isEmpty());
updateCounts();
}
@Override
protected void onResume() {
super.onResume();
GpsHistoryManager.getInstance().addListener(mRefreshRunnable);
refreshData();
}
@Override
protected void onPause() {
super.onPause();
GpsHistoryManager.getInstance().removeListener(mRefreshRunnable);
}
private void refreshData() {
List<GpsHistoryRecord> records = GpsHistoryManager.getInstance().getRecords();
mAdapter.setData(records);
mAdapter.notifyDataSetChanged();
updateEmptyHint(records.isEmpty());
updateCounts();
}
private void updateCounts() {
mTvSystemCount.setText(String.valueOf(GpsHistoryManager.getInstance().getSystemCount()));
mTvSimCount.setText(String.valueOf(GpsHistoryManager.getInstance().getSimCount()));
}
private void updateEmptyHint(boolean empty) {
mEmptyHint.setVisibility(empty ? View.VISIBLE : View.GONE);
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
}
private static class GpsHistoryAdapter extends BaseAdapter {
private List<GpsHistoryRecord> mData;
private final SimpleDateFormat mTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
GpsHistoryAdapter(List<GpsHistoryRecord> data) {
mData = data;
}
void setData(List<GpsHistoryRecord> data) {
mData = data;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = View.inflate(parent.getContext(), R.layout.list_item_gps_history, null);
}
GpsHistoryRecord item = mData.get(position);
LinearLayout root = (LinearLayout) convertView.findViewById(R.id.layout_record_root);
TextView tvType = (TextView) convertView.findViewById(R.id.tv_record_type);
TextView tvTime = (TextView) convertView.findViewById(R.id.tv_record_time);
TextView tvCoord = (TextView) convertView.findViewById(R.id.tv_record_coord);
String timeStr = mTimeFormat.format(new Date(item.getLocationTime()));
String coordStr = String.format(Locale.getDefault(),
"纬度: %.6f 经度: %.6f SID: %s",
item.getLatitude(), item.getLongitude(), item.getSid());
tvTime.setText(timeStr);
tvCoord.setText(coordStr);
if (item.isSim()) {
root.setBackgroundColor(Color.parseColor("#2a3a2a"));
tvType.setText("[模拟数据]");
tvType.setTextColor(Color.parseColor("#ffb74d"));
} else {
root.setBackgroundColor(Color.parseColor("#1c1c1c"));
tvType.setText("[系统数据]");
tvType.setTextColor(Color.parseColor("#4fc3f7"));
}
return convertView;
}
}
}

View File

@@ -0,0 +1,150 @@
package cc.winboll.studio.gitsion;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;
import cc.winboll.studio.gitsion.db.GpsHistoryDatabaseHelper;
import cc.winboll.studio.gitsion.model.GpsHistoryRecord;
import cc.winboll.studio.libgitsion.model.GpsSubscribeResult;
public final class GpsHistoryManager {
private static final int MAX_RECORDS = 2000;
private static final int TRIM_COUNT = 1000;
private static final GpsHistoryManager sInstance = new GpsHistoryManager();
private final List<Runnable> mListeners = new ArrayList<Runnable>();
private GpsHistoryDatabaseHelper mDbHelper;
private GpsHistoryManager() {}
public static GpsHistoryManager getInstance() {
return sInstance;
}
public void init(Context context) {
mDbHelper = new GpsHistoryDatabaseHelper(context.getApplicationContext());
}
public void addRecord(GpsSubscribeResult result, int type) {
if (mDbHelper == null) return;
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put(GpsHistoryDatabaseHelper.COL_LATITUDE, result.getLatitude());
cv.put(GpsHistoryDatabaseHelper.COL_LONGITUDE, result.getLongitude());
cv.put(GpsHistoryDatabaseHelper.COL_LOCATION_TIME, result.getLocationTime());
cv.put(GpsHistoryDatabaseHelper.COL_RECORD_TIME, System.currentTimeMillis());
cv.put(GpsHistoryDatabaseHelper.COL_TYPE, type);
cv.put(GpsHistoryDatabaseHelper.COL_SID, result.getSubscribeUniqueId());
cv.put(GpsHistoryDatabaseHelper.COL_DESC, result.getResultDesc());
db.insert(GpsHistoryDatabaseHelper.TABLE_NAME, null, cv);
trimIfNeeded(db);
notifyListeners();
}
public void addSystemRecord(GpsSubscribeResult result) {
addRecord(result, GpsHistoryDatabaseHelper.TYPE_SYSTEM);
}
public void addSimRecord(GpsSubscribeResult result) {
addRecord(result, GpsHistoryDatabaseHelper.TYPE_SIM);
}
public List<GpsHistoryRecord> getRecords() {
List<GpsHistoryRecord> list = new ArrayList<GpsHistoryRecord>();
if (mDbHelper == null) return list;
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor c = db.query(GpsHistoryDatabaseHelper.TABLE_NAME, null, null, null,
null, null, GpsHistoryDatabaseHelper.COL_ID + " DESC", null);
while (c.moveToNext()) {
list.add(parseCursor(c));
}
c.close();
return list;
}
public int getSystemCount() {
return countByType(GpsHistoryDatabaseHelper.TYPE_SYSTEM);
}
public int getSimCount() {
return countByType(GpsHistoryDatabaseHelper.TYPE_SIM);
}
public void clear() {
if (mDbHelper == null) return;
mDbHelper.getWritableDatabase().delete(GpsHistoryDatabaseHelper.TABLE_NAME, null, null);
notifyListeners();
}
public void addListener(Runnable listener) {
synchronized (mListeners) {
mListeners.add(listener);
}
}
public void removeListener(Runnable listener) {
synchronized (mListeners) {
mListeners.remove(listener);
}
}
private void notifyListeners() {
synchronized (mListeners) {
for (Runnable r : mListeners) {
r.run();
}
}
}
private int countByType(int type) {
if (mDbHelper == null) return 0;
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor c = db.rawQuery(
"SELECT COUNT(*) FROM " + GpsHistoryDatabaseHelper.TABLE_NAME
+ " WHERE " + GpsHistoryDatabaseHelper.COL_TYPE + "=?",
new String[]{String.valueOf(type)});
int count = 0;
if (c.moveToFirst()) {
count = c.getInt(0);
}
c.close();
return count;
}
private void trimIfNeeded(SQLiteDatabase db) {
Cursor c = db.rawQuery(
"SELECT COUNT(*) FROM " + GpsHistoryDatabaseHelper.TABLE_NAME, null);
int total = 0;
if (c.moveToFirst()) {
total = c.getInt(0);
}
c.close();
if (total > MAX_RECORDS) {
db.execSQL("DELETE FROM " + GpsHistoryDatabaseHelper.TABLE_NAME
+ " WHERE " + GpsHistoryDatabaseHelper.COL_ID + " IN ("
+ " SELECT " + GpsHistoryDatabaseHelper.COL_ID
+ " FROM " + GpsHistoryDatabaseHelper.TABLE_NAME
+ " ORDER BY " + GpsHistoryDatabaseHelper.COL_ID + " ASC"
+ " LIMIT " + TRIM_COUNT + ")");
}
}
private static GpsHistoryRecord parseCursor(Cursor c) {
return new GpsHistoryRecord(
c.getLong(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_ID)),
c.getDouble(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_LATITUDE)),
c.getDouble(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_LONGITUDE)),
c.getLong(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_LOCATION_TIME)),
c.getLong(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_RECORD_TIME)),
c.getInt(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_TYPE)),
c.getString(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_SID)),
c.getString(c.getColumnIndexOrThrow(GpsHistoryDatabaseHelper.COL_DESC))
);
}
}

View File

@@ -0,0 +1,28 @@
package cc.winboll.studio.gitsion;
import android.content.Intent;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.LocationPoint;
import cc.winboll.studio.libgitsion.service.GpsSubscribeReceiverService;
public final class GpsReceiverChildService1 extends GpsSubscribeReceiverService {
public static final String TAG = "GpsReceiverChildService1";
@Override
public void onReceiveGpsData(LocationPoint point, GpsSubscribeMsg config) {
super.onReceiveGpsData(point, config);
//当前独立接收日志
LogUtils.d(TAG,"独立接收服务1 成功收到GPS消息");
LogUtils.d(TAG,"纬度:"+point.getLatitude()+" 经度:"+point.getLongitude());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_NOT_STICKY;
}
}

View File

@@ -0,0 +1,27 @@
package cc.winboll.studio.gitsion;
import android.content.Intent;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.LocationPoint;
import cc.winboll.studio.libgitsion.service.GpsSubscribeReceiverService;
public final class GpsReceiverChildService2 extends GpsSubscribeReceiverService {
public static final String TAG = "GpsReceiverChildService2";
@Override
public void onReceiveGpsData(LocationPoint point, GpsSubscribeMsg config) {
super.onReceiveGpsData(point, config);
LogUtils.d(TAG,"独立接收服务2 成功收到GPS消息");
LogUtils.d(TAG,"纬度:"+point.getLatitude()+" 经度:"+point.getLongitude());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_NOT_STICKY;
}
}

View File

@@ -0,0 +1,27 @@
package cc.winboll.studio.gitsion;
import android.content.Intent;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.LocationPoint;
import cc.winboll.studio.libgitsion.service.GpsSubscribeReceiverService;
public final class GpsReceiverChildService3 extends GpsSubscribeReceiverService {
public static final String TAG = "GpsReceiverChildService3";
@Override
public void onReceiveGpsData(LocationPoint point, GpsSubscribeMsg config) {
super.onReceiveGpsData(point, config);
LogUtils.d(TAG,"独立接收服务3 成功收到GPS消息");
LogUtils.d(TAG,"纬度:"+point.getLatitude()+" 经度:"+point.getLongitude());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_NOT_STICKY;
}
}

View File

@@ -0,0 +1,395 @@
package cc.winboll.studio.gitsion;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.gitsion.R;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* WinBoLL Studio
* GPSRelaySentinel 主控制页面
* Java7 | API26~30
* 新增:模拟模式勾选控制 + 按钮互斥可用状态
*/
public final class MainActivity extends AppCompatActivity {
//原有控件
private Toolbar mToolbar;
private Switch mSwitchService;
//新增
private CheckBox mCheckBoxSimMode;
private Button btnSendLastGps;
private Spinner spinDirection;
private EditText etSimDistance;
private TextView tvTargetPreview;
private Button btnSimSend;
//全局模式标识 供给MainService判断
public static boolean IS_GPS_SIM_MODE = false;
//最后真实GPS坐标
public static double lastLat = 30.5928;
public static double lastLng = 114.3055;
//全局模拟坐标 供给MainService使用
public static double simLat = 30.5928;
public static double simLng = 114.3055;
//方位对应角度(正北0° 顺时针)
private double currentAngle = 0.0D;
//权限请求常量
private static final int REQUEST_LOCATION_PERMISSION = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initToolbar();
initSwitchEvent();
initSimPanelEvent();
initSimModeCheck();
ToastUtils.show("onCreate");
}
/**
* 全部控件绑定
*/
private void initView() {
//原有
mToolbar = findViewById(R.id.toolbar);
mSwitchService = findViewById(R.id.switch_service);
//新增
mCheckBoxSimMode = findViewById(R.id.checkbox_sim_mode);
btnSendLastGps = findViewById(R.id.btn_send_last_gps);
spinDirection = findViewById(R.id.spin_direction);
etSimDistance = findViewById(R.id.et_sim_distance);
tvTargetPreview = findViewById(R.id.tv_target_point_preview);
btnSimSend = findViewById(R.id.btn_sim_send_gps);
//方位下拉 全局灰色文字
ArrayAdapter<CharSequence> dirAdapter = ArrayAdapter.createFromResource(
this,
R.array.direction_list,
R.layout.spinner_item_gray
);
dirAdapter.setDropDownViewResource(R.layout.spinner_item_gray);
spinDirection.setAdapter(dirAdapter);
//初始化开关状态
mSwitchService.setChecked(hasLocationPermission());
refreshButtonEnableStatus();
refreshTargetPreview();
}
//模拟勾选框监听
private void initSimModeCheck() {
mCheckBoxSimMode.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
IS_GPS_SIM_MODE = isChecked;
refreshButtonEnableStatus();
if (isChecked) {
ToastUtils.show("已进入GPS模拟模式");
} else {
ToastUtils.show("退出模拟模式使用真实GPS");
}
}
});
}
//刷新按钮互斥可用状态
private void refreshButtonEnableStatus() {
if (IS_GPS_SIM_MODE) {
//模拟模式:真实按钮禁用、模拟按钮可用
btnSendLastGps.setEnabled(false);
btnSimSend.setEnabled(true);
} else {
//正常模式:真实可用、模拟禁用
btnSendLastGps.setEnabled(true);
btnSimSend.setEnabled(false);
}
}
/**
* 初始化标题栏
*/
private void initToolbar() {
setSupportActionBar(mToolbar);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
if (GlobalApplication.isDebugging()) {
menu.setGroupVisible(R.id.group_debug, true);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
}
if (item.getItemId() == R.id.action_gps_history) {
startActivity(new Intent(this, GpsHistoryActivity.class));
return true;
}
if (item.getItemId() == R.id.action_app_log) {
LogActivity.startLogActivity(this, false);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* GPS服务开关监听
*/
private void initSwitchEvent() {
mSwitchService.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
if (hasLocationPermission()) {
startGpsService();
} else {
requestLocationPermission();
mSwitchService.setChecked(false);
}
} else {
stopGpsService();
}
}
});
}
/**
* 模拟发送面板 全部事件初始化
*/
private void initSimPanelEvent() {
//1.原按钮发送最后一条真实GPS
btnSendLastGps.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendLastRealGpsBroadcast();
}
});
//2.方位下拉选择 -> 切换角度并刷新预览
spinDirection.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
currentAngle = getDirectionAngle(position);
refreshTargetPreview();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
//3.距离输入变化自动预览
etSimDistance.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
refreshTargetPreview();
}
}
});
//4.模拟发送按钮:计算偏移并赋值全局模拟坐标
btnSimSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveSimGpsData();
GpsHistoryManager.getInstance().addSimRecord(new cc.winboll.studio.libgitsion.model.GpsSubscribeResult(
"SIM",
cc.winboll.studio.libgitsion.model.GpsSubscribeConst.RESULT_SUCCESS,
"模拟GPS定位",
cc.winboll.studio.libgitsion.model.GpsSubscribeConst.GPS_STATE_LOCATED,
0,
System.currentTimeMillis(),
simLat,
simLng,
System.currentTimeMillis()
));
ToastUtils.show("已设置当前模拟GPS坐标");
}
});
}
/**
* 保存模拟坐标到全局静态变量 供给MainService使用
*/
private void saveSimGpsData() {
String disText = etSimDistance.getText().toString().trim();
double distance = 10D;
try {
distance = Double.parseDouble(disText);
} catch (Exception e) {
ToastUtils.show("请输入合法距离");
return;
}
double[] target = calculateOffsetLatLng(lastLat, lastLng, distance, currentAngle);
simLat = target[0];
simLng = target[1];
refreshTargetPreview();
}
/**
* 根据下拉position获取对应方位角度
*/
private double getDirectionAngle(int pos) {
switch (pos) {
case 0: return 0.0D; //正北
case 1: return 180.0D; //正南
case 2: return 90.0D; //正东
case 3: return 270.0D; //正西
case 4: return 45.0D; //东北
case 5: return 315.0D; //西北
case 6: return 135.0D; //东南
case 7: return 225.0D; //西南
default:return 0.0D;
}
}
/**
* 根据基准坐标+距离+角度 计算偏移经纬度
*/
private double[] calculateOffsetLatLng(double lat, double lng, double distanceMeter, double angle) {
double radAngle = Math.toRadians(angle);
double radLat = Math.toRadians(lat);
double meterPerLat = 111320D;
double meterPerLng = Math.cos(radLat) * 111320D;
double offsetLat = (distanceMeter * Math.cos(radAngle)) / meterPerLat;
double offsetLng = (distanceMeter * Math.sin(radAngle)) / meterPerLng;
return new double[]{lat + offsetLat , lng + offsetLng};
}
/**
* 刷新目标坐标预览
*/
private void refreshTargetPreview() {
String disText = etSimDistance.getText().toString().trim();
double distance = 10D;
try {
distance = Double.parseDouble(disText);
} catch (Exception e) {}
double[] target = calculateOffsetLatLng(lastLat, lastLng, distance, currentAngle);
String info = "目标模拟坐标:"
+ String.format("%.6f", target[0])
+ " , "
+ String.format("%.6f", target[1]);
tvTargetPreview.setText(info);
}
/**
* 发送【最后真实GPS】广播
*/
private void sendLastRealGpsBroadcast() {
Intent broadcast = new Intent("GPS_DATA_BROADCAST");
broadcast.putExtra("isSim", false);
broadcast.putExtra("lat", lastLat);
broadcast.putExtra("lng", lastLng);
sendBroadcast(broadcast);
LogUtils.d("GPS_SEND", "发送真实GPS -> lat:" + lastLat + " lng:" + lastLng);
}
//—————— 原有权限 & 服务启停 完全原样保留 ——————
private boolean hasLocationPermission() {
boolean basicPermission = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|| checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
if (basicPermission && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
return checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
return basicPermission;
}
private void requestLocationPermission() {
String[] permissionArray;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
permissionArray = new String[]{
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
};
} else {
permissionArray = new String[]{
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
};
}
requestPermissions(permissionArray, REQUEST_LOCATION_PERMISSION);
}
private void startGpsService() {
Intent serviceIntent = new Intent(MainActivity.this, MainService.class);
startForegroundService(serviceIntent);
ToastUtils.show("GPS Service started");
LogUtils.d(MainService.TAG, "GPS Service started from MainActivity");
}
private void stopGpsService() {
getSharedPreferences(MainService.PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(MainService.KEY_SERVICE_ENABLED, false)
.apply();
Intent serviceIntent = new Intent(MainActivity.this, MainService.class);
stopService(serviceIntent);
ToastUtils.show("GPS Service stopped");
LogUtils.d(MainService.TAG, "GPS Service stopped from MainActivity");
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mSwitchService.setChecked(true);
startGpsService();
} else {
ToastUtils.show("需要位置权限才能使用GPS服务");
mSwitchService.setChecked(false);
}
}
}
}

View File

@@ -0,0 +1,304 @@
package cc.winboll.studio.gitsion;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgitsion.manager.GpsSubscribeManager;
import cc.winboll.studio.libgitsion.manager.SubscribeLocationManager;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.GpsSubscribeResult;
import java.util.Map;
/**
* WinBoLL Studio
* GPS定位核心前台服务
* 负责GPS持续监听、订阅者步长判断、基准坐标刷新、前台常驻通知
* Java7 | API26~30
* 新增实时同步最新GPS到MainActivity静态坐标
*/
public final class MainService extends Service {
//日志标签
public static final String TAG = "MainService";
//前台通知常量
private static final String CHANNEL_ID = "gps_relay_channel";
private static final int NOTIFICATION_ID = 1;
//SP配置常量
static final String PREF_NAME = "gps_relay_service_prefs";
static final String KEY_SERVICE_ENABLED = "service_enabled";
//系统定位 & 通知控件
private LocationManager mLocationManager;
private LocationListener mLocationListener;
private NotificationManager mNotificationManager;
private NotificationCompat.Builder mNotificationBuilder;
//运行状态 & 计数
private boolean mIsRunning = false;
private int mGpsLocationCount = 0;
//订阅管理器
private GpsSubscribeManager mSubscribeManager;
private SubscribeLocationManager mLocationRuleManager;
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "Service onCreate");
initManager();
initNotificationConfig();
//上次开启状态则自动重启GPS监听
if (checkServiceEnableStatus()) {
LogUtils.d(TAG, "历史服务已启用自动启动GPS监听");
startGpsLocationListen();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "Service onStartCommand");
saveServiceEnableStatus(true);
startGpsLocationListen();
return START_STICKY;
}
/**
* 初始化订阅规则管理器
*/
private void initManager() {
mSubscribeManager = GpsSubscribeManager.getInstance();
mSubscribeManager.initContext(this);
mLocationRuleManager = SubscribeLocationManager.getInstance();
}
/**
* 初始化通知渠道与管理类
*/
private void initNotificationConfig() {
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
createSystemNotificationChannel();
}
/**
* 读取服务启用状态
*/
private boolean checkServiceEnableStatus() {
return getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getBoolean(KEY_SERVICE_ENABLED, false);
}
/**
* 保存服务启用状态
*/
private void saveServiceEnableStatus(boolean enabled) {
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_SERVICE_ENABLED, enabled)
.apply();
LogUtils.d(TAG, "服务启用状态已设置:" + enabled);
}
/**
* 启动GPS定位监听核心逻辑
*/
private void startGpsLocationListen() {
if (mIsRunning) {
LogUtils.d(TAG, "GPS监听已正在运行无需重复启动");
return;
}
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
initLocationListener();
try {
if (mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
//定位间隔1000毫秒 / 最小位移1米
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000,
1,
mLocationListener
);
mIsRunning = true;
startServiceForegroundNotification();
LogUtils.d(TAG, "GPS定位监听已成功注册");
}
} catch (SecurityException e) {
LogUtils.e(TAG, "定位权限缺失,监听启动失败:" + e.getMessage());
}
}
/**
* 初始化定位监听回调
*/
private void initLocationListener() {
mLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
handleLocationUpdate(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
LogUtils.d(TAG, "GPS状态变更 -> 提供者:" + provider + " 状态:" + status);
}
@Override
public void onProviderEnabled(String provider) {
LogUtils.d(TAG, "GPS提供者已启用" + provider);
}
@Override
public void onProviderDisabled(String provider) {
LogUtils.d(TAG, "GPS提供者已禁用" + provider);
}
};
}
/**
* 处理每次定位刷新|核心:步长判断 + 基准坐标更新
* 新增同步最新坐标到MainActivity静态变量
*/
private void handleLocationUpdate(Location location) {
mGpsLocationCount ++;
String locationInfo = "纬度:" + location.getLatitude() + " , 经度:" + location.getLongitude();
LogUtils.d(TAG, "定位刷新 -> " + locationInfo);
//========== 新增关键代码实时同步最新真实GPS坐标 ==========
MainActivity.lastLat = location.getLatitude();
MainActivity.lastLng = location.getLongitude();
//==========================================================
//更新前台通知文案
updateForegroundNotification(locationInfo);
double currentLat = location.getLatitude();
double currentLng = location.getLongitude();
long currentTime = location.getTime();
//保存每一次系统原始定位数据到历史记录
GpsSubscribeResult rawRecord = new GpsSubscribeResult(
"",
GpsSubscribeConst.RESULT_SUCCESS,
"系统GPS定位",
GpsSubscribeConst.GPS_STATE_LOCATED,
0,
System.currentTimeMillis(),
currentLat,
currentLng,
currentTime
);
GpsHistoryManager.getInstance().addSystemRecord(rawRecord);
//遍历全部订阅者进行推送规则判断
Map<String, GpsSubscribeMsg> subscribeAllMap = mSubscribeManager.getSubscribeMap();
for (Map.Entry<String, GpsSubscribeMsg> entry : subscribeAllMap.entrySet()) {
final String subscribeSid = entry.getKey();
final GpsSubscribeMsg subscribeConfig = entry.getValue();
//判断是否满足推送条件(全订阅/步长阈值)
boolean allowPush = mLocationRuleManager.isNeedPush(subscribeSid, currentLat, currentLng);
if (allowPush) {
//推送成功后刷新该订阅者基准定点坐标
mLocationRuleManager.updateSubscriberPoint(subscribeSid, currentLat, currentLng);
mLocationRuleManager.addPushCount(subscribeSid);
//发送结果广播给订阅者
GpsSubscribeResult result = new GpsSubscribeResult(
subscribeSid,
GpsSubscribeConst.RESULT_SUCCESS,
"GPS定位推送",
GpsSubscribeConst.GPS_STATE_LOCATED,
0,
System.currentTimeMillis(),
currentLat,
currentLng,
currentTime
);
mSubscribeManager.sendSubscribeResult(result);
GpsHistoryManager.getInstance().addSystemRecord(result);
LogUtils.d(TAG, "推送GPS数据至订阅者 SID" + subscribeSid);
}
}
}
/**
* 创建系统通知渠道
*/
private void createSystemNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(
CHANNEL_ID,
"GPS Relay Service",
NotificationManager.IMPORTANCE_LOW
);
notificationChannel.setDescription("GPSRelaySentinel 后台常驻服务通知");
mNotificationManager.createNotificationChannel(notificationChannel);
}
}
/**
* 开启前台常驻通知
*/
private void startServiceForegroundNotification() {
mNotificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("GPS 中继服务")
.setContentText("等待GPS定位数据...")
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setOngoing(true);
Notification notification = mNotificationBuilder.build();
startForeground(NOTIFICATION_ID, notification);
}
/**
* 动态更新通知内容
*/
private void updateForegroundNotification(String locationText) {
if (mNotificationBuilder != null) {
mNotificationBuilder.setContentText(locationText + " | 定位次数:" + mGpsLocationCount);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
//注销定位监听
if (mLocationManager != null && mLocationListener != null) {
try {
mLocationManager.removeUpdates(mLocationListener);
} catch (SecurityException e) {
LogUtils.e(TAG, "移除定位监听权限异常:" + e.getMessage());
}
}
mIsRunning = false;
LogUtils.d(TAG, "MainService 已销毁GPS监听已停止");
}
}

View File

@@ -0,0 +1,51 @@
package cc.winboll.studio.gitsion.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public final class GpsHistoryDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "gps_history.db";
private static final int DB_VERSION = 1;
public static final String TABLE_NAME = "gps_history";
public static final String COL_ID = "_id";
public static final String COL_LATITUDE = "latitude";
public static final String COL_LONGITUDE = "longitude";
public static final String COL_LOCATION_TIME = "location_time";
public static final String COL_RECORD_TIME = "record_time";
public static final String COL_TYPE = "type";
public static final String COL_SID = "sid";
public static final String COL_DESC = "description";
public static final int TYPE_SYSTEM = 0;
public static final int TYPE_SIM = 1;
private static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + " ("
+ COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COL_LATITUDE + " REAL NOT NULL, "
+ COL_LONGITUDE + " REAL NOT NULL, "
+ COL_LOCATION_TIME + " INTEGER NOT NULL, "
+ COL_RECORD_TIME + " INTEGER NOT NULL, "
+ COL_TYPE + " INTEGER NOT NULL DEFAULT " + TYPE_SYSTEM + ", "
+ COL_SID + " TEXT, "
+ COL_DESC + " TEXT"
+ ")";
public GpsHistoryDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}

View File

@@ -0,0 +1,64 @@
package cc.winboll.studio.gitsion.model;
import cc.winboll.studio.gitsion.db.GpsHistoryDatabaseHelper;
public final class GpsHistoryRecord {
private final long id;
private final double latitude;
private final double longitude;
private final long locationTime;
private final long recordTime;
private final int type;
private final String sid;
private final String description;
public GpsHistoryRecord(long id, double latitude, double longitude,
long locationTime, long recordTime,
int type, String sid, String description) {
this.id = id;
this.latitude = latitude;
this.longitude = longitude;
this.locationTime = locationTime;
this.recordTime = recordTime;
this.type = type;
this.sid = sid;
this.description = description;
}
public long getId() {
return id;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public long getLocationTime() {
return locationTime;
}
public long getRecordTime() {
return recordTime;
}
public int getType() {
return type;
}
public boolean isSim() {
return type == GpsHistoryDatabaseHelper.TYPE_SIM;
}
public String getSid() {
return sid;
}
public String getDescription() {
return description;
}
}

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
android:shape="rectangle">
<!-- 灰色边框 -->
<stroke
android:width="1dp"
android:color="#555555"/>
<!-- 内部深色背景 -->
<solid android:color="#222222"/>
<!-- 轻微圆角 -->
<corners android:radius="4dp"/>
</shape>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/aboutview"/>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#1c1c1c">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="8dp"
android:background="@drawable/border_gray"
android:layout_margin="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="系统GPS"
android:textColor="#aaaaaa"
android:textSize="13sp"/>
<TextView
android:id="@+id/tv_system_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#4fc3f7"
android:textSize="13sp"
android:layout_marginRight="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模拟GPS"
android:textColor="#aaaaaa"
android:textSize="13sp"/>
<TextView
android:id="@+id/tv_sim_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#ffb74d"
android:textSize="13sp"/>
</LinearLayout>
<TextView
android:id="@+id/tv_empty_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="暂无GPS历史数据"
android:textColor="#666666"
android:gravity="center"
android:padding="24dp"
android:visibility="gone"/>
<ListView
android:id="@+id/lv_gps_history"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View File

@@ -0,0 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#1c1c1c">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<!-- 数据面板容器 -->
<LinearLayout
android:id="@+id/container_data_panel"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GPSRelaySentinel"
android:textColor="#888888"
android:padding="6dp"
android:background="@drawable/border_gray"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp"
android:spacing="12dp">
<CheckBox
android:id="@+id/checkbox_sim_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模拟模式"
android:textColor="#999999"
android:padding="4dp"
android:background="@drawable/border_gray"
android:textSize="11sp"/>
<Switch
android:id="@+id/switch_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GPS Service"
android:textColor="#999999"
android:padding="4dp"
android:background="@drawable/border_gray"
android:checked="false"/>
<Button
android:id="@+id/btn_send_last_gps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送最后GPS"
android:textColor="#bbbbbb"
android:background="@drawable/border_gray"
android:textSize="12sp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp"
android:padding="12dp"
android:background="@drawable/border_gray">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模拟移动GPS发送面板"
android:textColor="#999999"
android:textSize="12sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:spacing="8dp">
<Spinner
android:id="@+id/spin_direction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/border_gray"/>
<EditText
android:id="@+id/et_sim_distance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="移动距离(米)"
android:inputType="numberDecimal"
android:text="10"
android:background="@drawable/border_gray"
android:textColor="#aaaaaa"
android:textColorHint="#666666"/>
</LinearLayout>
<TextView
android:id="@+id/tv_target_point_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="目标坐标:等待计算..."
android:textColor="#999999"
android:background="@drawable/border_gray"
android:padding="6dp"
android:textSize="11sp"
android:layout_marginTop="8dp"/>
<Button
android:id="@+id/btn_sim_send_gps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送模拟移动GPS"
android:textColor="#bbbbbb"
android:background="@drawable/border_gray"
android:layout_marginTop="10dp"/>
</LinearLayout>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<!-- 订阅面板容器 -->
<LinearLayout
android:id="@+id/container_subscribe_panel"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="12dp">
<cc.winboll.studio.libgitsion.view.GpsSubscribeControlView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/border_gray"/>
<cc.winboll.studio.libgitsion.view.GpsSubscribeControlView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/border_gray"/>
<cc.winboll.studio.libgitsion.view.GpsSubscribeControlView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/border_gray"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_record_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="@drawable/border_gray"
android:layout_margin="4dp">
<TextView
android:id="@+id/tv_record_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:padding="2dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:layout_marginBottom="2dp"/>
<TextView
android:id="@+id/tv_record_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#aaaaaa"
android:textSize="11sp"/>
<TextView
android:id="@+id/tv_record_coord"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="#999999"
android:gravity="center_vertical"/>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_about"
android:title="About"
android:icon="@android:drawable/ic_menu_info_details"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_gps_history"
android:title="GPS历史"
android:icon="@android:drawable/ic_menu_myplaces"
app:showAsAction="ifRoom"/>
<group android:id="@+id/group_debug">
<item
android:id="@+id/action_app_log"
android:title="应用日志"
android:visible="false"/>
</group>
</menu>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="direction_list">
<item>正北</item>
<item>正南</item>
<item>正东</item>
<item>正西</item>
<item>东北</item>
<item>西北</item>
<item>东南</item>
<item>西南</item>
</string-array>
</resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#FF9800</color>
<color name="mainWindowBackgroundColor">#FFF5F5F5</color>
<color name="mainWindowTextColor">#FF000000</color>
<color name="mainWindowDarkBackgroundColor">#FFF5F5F5</color>
<color name="mainWindowDarkTextColor">#FF000000</color>
<color name="toolbarBackgroundColor">#FF009688</color>
<color name="toolbarTextColor">#FF000000</color>
<color name="debugTextColor">#FF808080</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Gitsion</string>
<string name="app_description">A GPS relay tool supporting real and simulated positioning, running in background for location forwarding, debugging and simulation.</string>
</resources>

View File

@@ -0,0 +1,35 @@
<resources>
<!-- Base application theme. -->
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="themeDebug">@style/DebugActivityTheme</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowDarkBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowDarkTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowTextColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
</style>
<style name="DebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>

View File

@@ -1,21 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# 保持与旧版Gradle插件的兼容
android.disableAutomaticComponentCreation=true

0
gradlew vendored Executable file → Normal file
View File

View File

@@ -20,11 +20,15 @@ android {
}
}
// 米盟 SDK
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
@@ -53,7 +57,7 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
// 米盟
//api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
@@ -62,9 +66,9 @@ dependencies {
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libappbase:15.20.34'
//api 'cc.winboll.studio:libappbase:15.20.22'
// 备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33'
api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.22'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Jun 29 11:28:11 HKT 2026
stageCount=18
#Tue Jun 02 08:54:20 HKT 2026
stageCount=13
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.17
publishVersion=15.20.12
buildCount=0
baseBetaVersion=15.20.18
baseBetaVersion=15.20.13

View File

@@ -15,6 +15,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@@ -22,9 +23,9 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import cc.winboll.studio.libaes.DrawerMenuDataAdapter;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.models.DrawerMenuBean;
@@ -32,9 +33,11 @@ import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libaes.views.ADrawerMenuListView;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import com.baoyz.widget.PullRefreshLayout;
import java.util.ArrayList;
public abstract class DrawerFragmentActivity extends AppCompatActivity implements IWinBoLLActivity, AdapterView.OnItemClickListener {
@@ -88,10 +91,10 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
// 修复:释放广告资源,避免内存泄漏
// ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
// if (adsBannerView != null) {
// adsBannerView.releaseAdResources();
// }
ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
if (adsBannerView != null) {
adsBannerView.releaseAdResources();
}
}
@Override
@@ -148,10 +151,10 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
protected void onResume() {
super.onResume();
// ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
// if (adsBannerView != null) {
// adsBannerView.resumeADs(DrawerFragmentActivity.this);
// }
ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
if (adsBannerView != null) {
adsBannerView.resumeADs(DrawerFragmentActivity.this);
}
}
void initRootView() {

View File

@@ -9,10 +9,13 @@ import android.app.Activity;
import android.content.Context;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import java.util.ArrayList;
public class AESThemeUtil {
@@ -30,8 +33,7 @@ public class AESThemeUtil {
* 初始化主题样式ID集合
*/
public static void init(ArrayList<Integer> themeStyleIDList) {
if (themeStyleIDList == null) {
if(themeStyleIDList == null) {
themeStyleIDList = new ArrayList<Integer>();
AESThemeBean.fillThemeStyleIDList(themeStyleIDList);
}
@@ -43,7 +45,7 @@ public class AESThemeUtil {
* 获取当前主题样式ID
*/
public static int getThemeTypeID(Context context) {
AESThemeBean bean = AESThemeBean.loadBean(context, AESThemeBean.class);
AESThemeBean bean = AESThemeBean.loadBean(context, AESThemeBean.class);
return bean == null ? getThemeStyleID(AESThemeBean.ThemeType.AES) : bean.getCurrentThemeTypeID();
}

View File

@@ -0,0 +1,491 @@
package cc.winboll.studio.libaes.views;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.utils.MimoUtils;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import com.miui.zeus.mimo.sdk.ADParams;
import com.miui.zeus.mimo.sdk.BannerAd;
import com.miui.zeus.mimo.sdk.MimoCustomController;
import com.miui.zeus.mimo.sdk.MimoLocation;
import com.miui.zeus.mimo.sdk.MimoSdk;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/18 14:41
* @Describe WinBoLL 横幅广告类
*/
public class ADsBannerView extends LinearLayout {
public static final String TAG = "ADsBannerView";
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL_BETA = "d129ee5a263911f981a6dc7a9802e3e7";
private String BANNER_POS_ID_WINBOLL = "4ec30efdb32271765b9a4efac902828b";
/*
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL_BETA = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL = "802e356f1726f9ff39c69308bfd6f06a";
*/
Context mContext;
View mMianView;
SharedPreferences mSharedPreferences;
ViewGroup mContainer;
BannerAd mBannerAd;
List<BannerAd> mAllBanners = new ArrayList<>();
// 新增主线程Handler确保广告操作在主线程执行
private Handler mMainHandler;
public ADsBannerView(Context context) {
super(context);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context);
}
void initView(Context context) {
this.mContext = context;
initMimoSdk(this.mContext);
// 初始化主线程Handler关键确保广告操作在主线程执行
mMainHandler = new Handler(Looper.getMainLooper());
this.mMianView = inflate(this.mContext, R.layout.view_adsbanner, null);
mContainer = this.mMianView.findViewById(R.id.ads_container);
addView(this.mMianView);
}
public void resumeADs(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
// 2. 释放之前的广告资源
if (mBannerAd != null) {
mBannerAd.destroy();
}
return;
}
// 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行)
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
if (ADsControlView.getAdsModeFromStatic(this.mContext) == ADsMode.MIMO_SDK) {
LogUtils.i(TAG, "已设置播放米盟广告,正在播放...");
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 再次校验生命周期避免延迟执行时Activity已销毁
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
fetchAd(activity);
}
}
}, 1000); // 延迟1秒请求广告提升页面加载体验
}
}
}
/**
* 释放广告资源关键避免内存泄漏和空Context调用
*/
public void releaseAdResources() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "releaseAdResources()");
// 移除Handler回调
if (mMainHandler != null) {
mMainHandler.removeCallbacksAndMessages(null);
}
// 销毁所有广告实例
if (mAllBanners != null && !mAllBanners.isEmpty()) {
for (BannerAd ad : mAllBanners) {
if (ad != null) {
ad.destroy();
}
}
mAllBanners.clear();
}
// 置空当前广告引用
mBannerAd = null;
// 移除广告容器中的视图
if (mContainer != null) {
mContainer.removeAllViews();
}
}
/**
* 显示广告核心修复传递安全的Context + 生命周期校验)
*/
private void showAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "showAd()");
// 1. 生命周期校验避免Activity已销毁时操作UI
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "showAd: Activity is finishing or destroyed");
return;
}
// 2. 非空校验:广告实例和容器
if (mBannerAd == null || mContainer == null) {
LogUtils.e(TAG, "showAd: BannerAd or Container is null");
return;
}
// 3. 创建广告容器使用ApplicationContext避免内存泄漏
final FrameLayout container = new FrameLayout(activity.getApplicationContext());
container.setPadding(0, 0, 0, MimoUtils.dpToPx(activity, 10));
mContainer.addView(container, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
));
// if (mIsBiddingWin) {
// mBannerAd.setPrice(getPrice());
// }
// 4. 显示广告传递ApplicationContext避免Activity Context失效
mBannerAd.showAd(activity, container, new BannerAd.BannerInteractionListener() {
@Override
public void onAdClick() {
LogUtils.d(TAG, "onAdClick");
}
@Override
public void onAdShow() {
LogUtils.d(TAG, "onAdShow");
}
@Override
public void onAdDismiss() {
LogUtils.d(TAG, "onAdDismiss");
// 修复移除容器时校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
@Override
public void onRenderSuccess() {
LogUtils.d(TAG, "onRenderSuccess");
}
@Override
public void onRenderFail(int code, String msg) {
LogUtils.e(TAG, "onRenderFail errorCode " + code + " errorMsg " + msg);
// 修复:渲染失败时移除容器
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
});
}
/**
* 请求广告核心修复Context安全校验 + 异常捕获 + 资源管理)
*/
private void fetchAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "fetchAd()");
// 1. 双重校验Activity未销毁 + Context非空
if (activity == null || activity.isFinishing() || activity.isDestroyed() || activity.getApplicationContext() == null) {
LogUtils.e(TAG, "fetchAd: Invalid Context or Activity state");
return;
}
// 2. 释放之前的广告资源,避免内存泄漏
if (mBannerAd != null) {
mBannerAd.destroy();
}
// 3. 初始化广告使用ApplicationContext避免Activity Context失效
try {
mBannerAd = new BannerAd();
mAllBanners.add(mBannerAd);
} catch (Exception e) {
LogUtils.e(TAG, "fetchAd: Init BannerAd failed", e);
return;
}
// 4. 设置下载监听
mBannerAd.setDownLoadListener(new BannerAd.BannerDownloadListener() {
@Override
public void onDownloadStarted() {
LogUtils.d(TAG, "onDownloadStarted");
}
@Override
public void onDownloadPaused() {
LogUtils.d(TAG, "onDownloadPaused");
}
@Override
public void onDownloadFailed(int errorCode) {
String msg = "onDownloadFailed, errorCode = " + errorCode;
LogUtils.d(TAG, msg);
//ToastUtils.show(msg);
}
@Override
public void onDownloadFinished() {
LogUtils.d(TAG, "onDownloadFinished");
}
@Override
public void onDownloadProgressUpdated(int progress) {
LogUtils.d(TAG, "onDownloadProgressUpdated " + progress + "%");
}
@Override
public void onInstallFailed(int errorCode) {
LogUtils.d(TAG, "onInstallFailed, errorCode = " + errorCode);
}
@Override
public void onInstallStart() {
LogUtils.d(TAG, "onInstallStart");
}
@Override
public void onInstallSuccess() {
LogUtils.d(TAG, "onInstallSuccess");
}
@Override
public void onDownloadCancel() {
LogUtils.d(TAG, "onDownloadCancel");
}
});
// 5. 构建广告参数并请求
String currentAD_ID = getAD_ID();
LogUtils.d(TAG, String.format("currentAD_ID = %s", currentAD_ID));
ADParams params = new ADParams.Builder().setUpId(currentAD_ID).build();
mBannerAd.loadAd(params, new BannerAd.BannerLoadListener() {
@Override
public void onBannerAdLoadSuccess() {
LogUtils.d(TAG, "onBannerAdLoadSuccess()");
// 修复广告加载成功后校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
showAd(activity);
//ToastUtils.show("showAd()");
}
}
@Override
public void onAdLoadFailed(int errorCode, String errorMsg) {
String msg = "onAdLoadFailed: errorCode = " + errorCode + ", errorMsg = " + errorMsg;
LogUtils.d(TAG, msg);
removeAllBanners();
}
});
}
void removeAllBanners() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
// 修复:加载失败时移除当前广告实例
if (mAllBanners.contains(mBannerAd)) {
mAllBanners.remove(mBannerAd);
}
mBannerAd.destroy();
mBannerAd = null;
}
/**
* 根据当前秒数获取广告ID原逻辑保留
*/
private String getAD_ID() {
long currentSecond = System.currentTimeMillis() / 1000;
return (currentSecond % 2 == 0) ? BANNER_POS_ID :
(GlobalApplication.isDebugging() ? BANNER_POS_ID_WINBOLL_BETA : BANNER_POS_ID_WINBOLL);
}
/**
* 获取广告价格(原逻辑保留,添加空指针校验)
*/
// private long getPrice() {
// if (mBannerAd == null) {
// return 0;
// }
// Map<String, Object> map = mBannerAd.getMediaExtraInfo();
// if (map == null || map.isEmpty() || !map.containsKey("price")) {
// LogUtils.w(TAG, "getPrice: media extra info is null or no price key");
// return 0;
// }
// Object priceObj = map.get("price");
// if (priceObj instanceof Long) {
// return (Long) priceObj;
// } else if (priceObj instanceof Integer) {
// return ((Integer) priceObj).longValue();
// } else {
// LogUtils.e(TAG, "getPrice: price type is invalid");
// return 0;
// }
// }
/**
* 显示隐私协议弹窗原逻辑保留优化Context使用
*/
// private void showPrivacy() {
// // 校验Activity状态避免弹窗泄露
// if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
// return;
// }
// ADsMode adsMode = ADsControlView.getAdsModeFromStatic(this.mContext);
// if (adsMode == ADsMode.STANDALONE) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.STANDALONE);
// LogUtils.i(TAG, "单机模式,广告已处于不可用状态...");
// Toast.makeText(getActivity().getApplicationContext(), "单机模式,广告已处于不可用状态...", Toast.LENGTH_SHORT).show();
// return;
// } else if (adsMode == ADsMode.MIMO_SDK) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.MIMO_SDK);
// LogUtils.i(TAG, "米盟广告SDK支持模式现在初始化SDK...");
// initMimoSdk();
// return;
// }
// else {
// LogUtils.i(TAG, "开始弹出隐私协议...");
// AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// builder.setTitle("用户须知");
// builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
// builder.setIcon(R.drawable.ic_launcher);
// builder.setCancelable(false); // 点击对话框以外的区域不消失
// builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(1))
// .apply();
// initMimoSdk();
// dialog.dismiss();
// }
// });
// builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(0))
// .apply();
// dialog.dismiss();
// }
// });
// AlertDialog dialog = builder.create();
//
// // 配置弹窗位置(底部全屏)
// Window window = dialog.getWindow();
// if (window != null) {
// window.setGravity(Gravity.BOTTOM);
// WindowManager m = getActivity().getWindowManager();
// Display d = m.getDefaultDisplay();
// WindowManager.LayoutParams p = window.getAttributes();
// p.width = d.getWidth();
// window.setAttributes(p);
// }
// dialog.show();
// }
// }
/**
* 初始化米盟SDK核心修复传递ApplicationContext + 异常捕获)
*/
private void initMimoSdk(Context context) {
// 1. 安全获取ApplicationContext避免Activity Context失效
Context appContext = context.getApplicationContext();
if (appContext == null) {
LogUtils.e(TAG, "initMimoSdk: ApplicationContext is null");
return;
}
// 2. 初始化SDK捕获异常避免崩溃
try {
MimoSdk.init(appContext, new MimoCustomController() {
@Override
public boolean isCanUseLocation() {
return true;
}
@Override
public MimoLocation getMimoLocation() {
return null;
}
@Override
public boolean isCanUseWifiState() {
return true;
}
@Override
public boolean alist() {
return true;
}
}, new MimoSdk.InitCallback() {
@Override
public void success() {
LogUtils.d(TAG, "MimoSdk init success");
}
@Override
public void fail(int code, String msg) {
LogUtils.e(TAG, "MimoSdk init fail, code=" + code + ",msg=" + msg);
}
});
MimoSdk.setDebugOn(true);
} catch (Exception e) {
LogUtils.e(TAG, "initMimoSdk: init failed", e);
}
}
/**
* 获取SharedPreferences实例原逻辑保留添加空指针校验
*/
// SharedPreferences getSharedPreferences() {
//// if (mSharedPreferences == null) {
//// // 修复使用ApplicationContext获取SharedPreferences避免Activity Context泄露
//// Context appContext = getActivity().getApplicationContext();
//// if (appContext != null) {
//// mSharedPreferences = appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// } else {
//// LogUtils.e(TAG, "getSharedPreferences: ApplicationContext is null");
//// // 降级方案若ApplicationContext为空使用Activity Context仅作兼容
//// mSharedPreferences = getActivity().getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// }
//// }
// return mSharedPreferences;
// }
}

View File

@@ -0,0 +1,633 @@
package cc.winboll.studio.libaes.views;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.Html;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.PopupWindow;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.enums.PrivacyAgreeStatus;
import cc.winboll.studio.libaes.utils.WebUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import com.miui.zeus.mimo.sdk.MimoCustomController;
import com.miui.zeus.mimo.sdk.MimoLocation;
import com.miui.zeus.mimo.sdk.MimoSdk;
import java.lang.reflect.Field;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 17:51
* @LastEditTime 2026/01/08 11:00:00 HKT
* @Describe 广告模式控制控件Java 7 兼容,云宝物语模式)
* 核心修改将PopupMenu锚点绑定到view_popmenu_anchor_point控件菜单精准显示在锚点位置
*/
public class ADsControlView extends LinearLayout {
public static final String TAG = "ADsControlView";
// SP存储配置
private static final String SP_NAME = "ads_control_config";
private static final String KEY_SELECTED_MODE = "selected_ads_mode";
ADsMode mADsMode;
private static final String PRIVACY_VALUE = "privacy_value";
PrivacyAgreeStatus mPrivacyAgreeStatus;
// Handler消息标识
private static final int MSG_UPDATE_MODE = 1001;
// 控件引用
private RadioGroup rgADsMode;
private RadioButton rbStandalone;
private RadioButton rbMimoSDK;
private RadioButton rbStoreQrcode;
private RelativeLayout rlWinbollStore;
private ImageView ivWinbollStoreQrcode;
// 新增:锚点控件引用
private TextView viewPopmenuAnchorPoint;
// 外部监听、SP实例、Handler实例
private OnAdsModeSelectedListener listener;
private SharedPreferences sharedPreferences;
private InternalHandler mHandler;
private Context mContext;
// 静态列表:存储所有已创建的控件实例
private static final java.util.List<ADsControlView> sControlViews = new java.util.ArrayList<ADsControlView>();
// 常量定义
private static final String WECHAT_STORE_URL = "https://store.weixin.qq.com/shop/b/XhrPkZgoeHo4zug";
private static final int MENU_ITEM_OPEN_STORE = 1001;
// 构造方法Java 7 兼容)
public ADsControlView(Context context) {
super(context);
initView(context);
}
public ADsControlView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
@SuppressWarnings("deprecation")
public ADsControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public void setPrivacyAgreeStatus(PrivacyAgreeStatus privacyAgreeStatus) {
this.mPrivacyAgreeStatus = privacyAgreeStatus;
sharedPreferences.edit().putString(PRIVACY_VALUE, this.mPrivacyAgreeStatus.name()).apply();
}
public PrivacyAgreeStatus getPrivacyAgreeStatus() {
String privacyAgreeStatusStr = sharedPreferences.getString(PRIVACY_VALUE, PrivacyAgreeStatus.UN_SIGNED.name());
PrivacyAgreeStatus privacyAgreeStatus = PrivacyAgreeStatus.fromString(privacyAgreeStatusStr);
return privacyAgreeStatus;
}
public void setADsMode(ADsMode mADsMode) {
this.mADsMode = mADsMode;
sharedPreferences.edit().putString(KEY_SELECTED_MODE, this.mADsMode.name()).apply();
updateStoreQrcodeLayoutVisibility(mADsMode);
}
public ADsMode getADsMode() {
String savedModeStr = sharedPreferences.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
mADsMode = ADsMode.fromValue(savedModeStr);
return mADsMode;
}
/**
* 初始化视图、SP、Handler
*/
private void initView(final Context context) {
this.mContext = context;
// 加载布局
LayoutInflater.from(context).inflate(R.layout.view_adscontrol, this, true);
// 初始化SP
sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
// 绑定控件
rgADsMode = (RadioGroup) findViewById(R.id.rg_ads_mode);
rbStandalone = (RadioButton) findViewById(R.id.rb_standalone);
rbMimoSDK = (RadioButton) findViewById(R.id.rb_mimo_sdk);
rbStoreQrcode = (RadioButton) findViewById(R.id.rb_store_qrcode);
rlWinbollStore = (RelativeLayout) findViewById(R.id.rl_winboll_store);
ivWinbollStoreQrcode = (ImageView) findViewById(R.id.iv_winboll_store);
// 绑定锚点控件
viewPopmenuAnchorPoint = (TextView) findViewById(R.id.view_popmenu_anchor_point);
// 初始化Handler
mHandler = new InternalHandler(Looper.getMainLooper());
// 核心修改初始化图片的点击和长按事件锚点改为view_popmenu_anchor_point
initImageViewClickAndLongClick();
// 注册控件实例
registerControlView(this);
// 从SP读取初始模式
setSelectedMode(getADsMode());
// 单选组选择事件监听
rgADsMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
if (checkedId == R.id.rb_standalone) {
setADsMode(ADsMode.STANDALONE);
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
} else if (checkedId == R.id.rb_mimo_sdk) {
handlePrivacyLogic((Activity) context, PrivacyAgreeStatus.UN_SIGNED, new OnPrivacyChangeListener() {
@Override
public void onAgreePrivacy() {
setADsMode(ADsMode.MIMO_SDK);
if (listener != null) listener.onModeSelected(ADsMode.MIMO_SDK);
}
@Override
public void onDisagreePrivacy() {
setADsMode(ADsMode.STANDALONE);
setSelectedMode(ADsMode.STANDALONE);
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
}
});
} else if (checkedId == R.id.rb_store_qrcode) {
setADsMode(ADsMode.STORE_QRCODE);
if (listener != null) listener.onModeSelected(ADsMode.STORE_QRCODE);
}
}
});
}
/**
* 初始化图片的点击和长按事件
* 核心将PopupMenu锚点绑定到view_popmenu_anchor_point控件
*/
private void initImageViewClickAndLongClick() {
if (ivWinbollStoreQrcode == null || viewPopmenuAnchorPoint == null) {
LogUtils.e(TAG, "initImageViewClickAndLongClick: 控件引用为空");
return;
}
// 1. 点击事件:简化为提示信息
ivWinbollStoreQrcode.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ToastUtils.show("长按图片可打开微信小店");
LogUtils.d(TAG, "图片点击:提示用户长按打开微信小店");
}
});
// 2. 长按事件锚点改为view_popmenu_anchor_point
ivWinbollStoreQrcode.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// 计算锚点控件的屏幕坐标(用于菜单位置微调)
int[] anchorLocation = new int[2];
viewPopmenuAnchorPoint.getLocationOnScreen(anchorLocation);
final int anchorX = anchorLocation[0];
final int anchorY = anchorLocation[1];
// 创建PopupMenu锚点绑定到view_popmenu_anchor_point
PopupMenu popupMenu = new PopupMenu(mContext, viewPopmenuAnchorPoint);
// 设置菜单重力:相对锚点居中显示
popupMenu.setGravity(Gravity.CENTER);
Menu menu = popupMenu.getMenu();
menu.add(Menu.NONE, MENU_ITEM_OPEN_STORE, Menu.NONE, "打开微信小店");
// 设置菜单点击事件
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == MENU_ITEM_OPEN_STORE) {
WebUtils.openUrlInBrowser(mContext, WECHAT_STORE_URL);
return true;
}
return false;
}
});
try {
// 反射获取PopupWindow微调菜单位置可选
Field popupField = PopupMenu.class.getDeclaredField("mPopup");
popupField.setAccessible(true);
Object popupObject = popupField.get(popupMenu);
if (popupObject instanceof PopupWindow) {
final PopupWindow popupWindow = (PopupWindow) popupObject;
popupWindow.setAnimationStyle(0); // 关闭默认动画
// 延迟微调菜单位置(确保布局测量完成)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
int menuX = anchorX + viewPopmenuAnchorPoint.getWidth() / 2 - popupWindow.getWidth() / 2;
int menuY = anchorY + viewPopmenuAnchorPoint.getHeight() / 2 - popupWindow.getHeight() / 2;
if (!popupWindow.isShowing()) {
popupWindow.showAtLocation(viewPopmenuAnchorPoint, Gravity.NO_GRAVITY, menuX, menuY);
}
}
}, 30);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
LogUtils.e(TAG, "反射获取PopupWindow失败", e);
}
// 显示菜单
popupMenu.show();
LogUtils.d(TAG, "长按图片菜单锚点为view_popmenu_anchor_point");
return true;
}
});
// 设置控件可交互标识
ivWinbollStoreQrcode.setClickable(true);
ivWinbollStoreQrcode.setFocusable(true);
ivWinbollStoreQrcode.setLongClickable(true);
viewPopmenuAnchorPoint.setClickable(false); // 锚点控件不可点击
viewPopmenuAnchorPoint.setLongClickable(false);
}
/**
* 从ImageView中提取Bitmap保留方法无实际调用
*/
private Bitmap getBitmapFromImageView(ImageView imageView) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
return null;
}
/**
* 压缩Bitmap备用方法无实际调用
*/
private Bitmap compressBitmapBySize(Bitmap src, int maxWidth, int maxHeight) {
if (src == null) return null;
int width = src.getWidth();
int height = src.getHeight();
float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
int newWidth = (int) (width * scale);
int newHeight = (int) (height * scale);
return Bitmap.createScaledBitmap(src, newWidth, newHeight, true);
}
/**
* 计算Bitmap采样率备用方法无实际调用
*/
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* 从ImageView反射获取资源ID备用方法无实际调用
*/
private int getResIdFromImageView(ImageView imageView) {
try {
Field field = ImageView.class.getDeclaredField("mSrcResource");
field.setAccessible(true);
return field.getInt(imageView);
} catch (NoSuchFieldException | IllegalAccessException e) {
LogUtils.e(TAG, "getResIdFromImageView: 反射失败", e);
return 0;
}
}
/**
* 更新二维码布局显示状态
*/
private void updateStoreQrcodeLayoutVisibility(ADsMode mode) {
if (rlWinbollStore == null) return;
rlWinbollStore.setVisibility(mode == ADsMode.STORE_QRCODE ? View.VISIBLE : View.GONE);
}
/**
* 清理SP中的隐私协议状态
*/
public static void cleanPrivacyStatus(Context context) {
if (context == null) {
LogUtils.e(TAG, "cleanPrivacyStatus: Context is null");
return;
}
SharedPreferences sp = getPrivacySharedPreferences(context);
sp.edit().remove(PRIVACY_VALUE).apply();
LogUtils.i(TAG, "隐私协议状态清理成功");
ToastUtils.show("隐私协议状态已清理");
}
/**
* 获取隐私协议SP实例
*/
private static SharedPreferences getPrivacySharedPreferences(Context context) {
Context appContext = context.getApplicationContext();
if (appContext != null) {
return appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
}
return context.getSharedPreferences(PRIVACY_VALUE, Context.MODE_PRIVATE);
}
/**
* 处理隐私协议逻辑
*/
private static void handlePrivacyLogic(final Activity activity, PrivacyAgreeStatus privacyAgreeStatus, final OnPrivacyChangeListener onPrivacyChangeListener) {
if (privacyAgreeStatus == PrivacyAgreeStatus.REJECTED) {
Toast.makeText(activity.getApplicationContext(), "已拒绝隐私协议,广告已处于不可用状态", Toast.LENGTH_SHORT).show();
return;
} else if (privacyAgreeStatus == PrivacyAgreeStatus.AGREED) {
initMimoSdkStatic(activity.getApplicationContext());
return;
} else {
AlertDialog dialog = createPrivacyDialog(activity, onPrivacyChangeListener);
Window window = dialog.getWindow();
if (window != null) {
window.setGravity(Gravity.BOTTOM);
WindowManager m = activity.getWindowManager();
Display d = m.getDefaultDisplay();
WindowManager.LayoutParams p = window.getAttributes();
p.width = d.getWidth();
window.setAttributes(p);
}
dialog.show();
}
}
/**
* 初始化米盟SDK
*/
private static void initMimoSdkStatic(Context appContext) {
if (appContext == null) return;
try {
MimoSdk.init(appContext, new MimoCustomController() {
@Override
public boolean isCanUseLocation() {
return true;
}
@Override
public MimoLocation getMimoLocation() {
return null;
}
@Override
public boolean isCanUseWifiState() {
return true;
}
@Override
public boolean alist() {
return true;
}
}, new MimoSdk.InitCallback() {
@Override
public void success() {
LogUtils.d(TAG, "米盟SDK初始化成功");
}
@Override
public void fail(int code, String msg) {
LogUtils.e(TAG, "米盟SDK初始化失败" + code + ", " + msg);
}
});
MimoSdk.setDebugOn(true);
} catch (Exception e) {
LogUtils.e(TAG, "米盟SDK初始化异常", e);
}
}
/**
* 静态方法更新SP中的模式
*/
public static void updateAdsModeByStatic(Context context, ADsMode mode) {
if (context == null || mode == null) return;
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_SELECTED_MODE, mode.name()).apply();
InternalHandler.sendUpdateModeMessage(mode);
}
/**
* 静态方法读取SP中的模式
*/
public static ADsMode getAdsModeFromStatic(Context context) {
if (context == null) return ADsMode.STANDALONE;
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String savedModeStr = sp.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
return ADsMode.fromValue(savedModeStr);
}
/**
* 注册控件实例
*/
private static void registerControlView(ADsControlView view) {
synchronized (sControlViews) {
if (!sControlViews.contains(view)) {
sControlViews.add(view);
}
}
}
/**
* 移除控件实例
*/
private static void unregisterControlView(ADsControlView view) {
synchronized (sControlViews) {
sControlViews.remove(view);
}
}
/**
* 设置选中模式
*/
private void setSelectedMode(final ADsMode mode) {
final ADsMode mode2 = (mode == null) ? ADsMode.STANDALONE : mode;
if (Looper.myLooper() == Looper.getMainLooper()) {
if (mode2 == ADsMode.STANDALONE) {
rbStandalone.setChecked(true);
} else if (mode2 == ADsMode.MIMO_SDK) {
rbMimoSDK.setChecked(true);
} else if (mode2 == ADsMode.STORE_QRCODE) {
rbStoreQrcode.setChecked(true);
}
updateStoreQrcodeLayoutVisibility(mode2);
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
setSelectedMode(mode2);
}
});
}
}
/**
* 获取选中模式
*/
public ADsMode getSelectedMode() {
int checkedId = rgADsMode.getCheckedRadioButtonId();
if (checkedId == R.id.rb_mimo_sdk) {
return ADsMode.MIMO_SDK;
} else if (checkedId == R.id.rb_store_qrcode) {
return ADsMode.STORE_QRCODE;
} else {
return ADsMode.STANDALONE;
}
}
/**
* 设置外部监听
*/
public void setOnAdsModeSelectedListener(OnAdsModeSelectedListener listener) {
this.listener = listener;
}
/**
* 内部Handler类
*/
private static class InternalHandler extends Handler {
static volatile InternalHandler _InternalHandler;
public InternalHandler(Looper looper) {
super(looper);
_InternalHandler = this;
}
public static void sendUpdateModeMessage(ADsMode mode) {
if (mode == null || _InternalHandler == null) return;
Message msg = _InternalHandler.obtainMessage();
msg.what = MSG_UPDATE_MODE;
msg.obj = mode;
_InternalHandler.sendMessage(msg);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_UPDATE_MODE) {
ADsMode mode = (ADsMode) msg.obj;
if (mode == null) return;
synchronized (sControlViews) {
for (ADsControlView view : sControlViews) {
if (view != null && view.isShown() && view.isAttachedToWindow()) {
view.setSelectedMode(mode);
view.updateStoreQrcodeLayoutVisibility(mode);
}
}
}
}
}
}
/**
* 生命周期:控件销毁
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
unregisterControlView(this);
}
/**
* 外部监听接口
*/
public interface OnAdsModeSelectedListener {
void onModeSelected(ADsMode selectedMode);
}
/**
* 隐私协议监听接口
*/
public interface OnPrivacyChangeListener {
void onAgreePrivacy();
void onDisagreePrivacy();
}
/**
* 创建隐私协议对话框
*/
private static AlertDialog createPrivacyDialog(final Activity activity, final OnPrivacyChangeListener onPrivacyChangeListener) {
View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_privacy_agreement, null);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(dialogView).setCancelable(false);
final AlertDialog dialog = builder.create();
final TextView tvPrivacyUrl = (TextView) dialogView.findViewById(R.id.tv_privacy_url);
Button btnAgree = (Button) dialogView.findViewById(R.id.btn_agree);
Button btnDisagree = (Button) dialogView.findViewById(R.id.btn_disagree);
tvPrivacyUrl.setText(Html.fromHtml("<u>" + tvPrivacyUrl.getText().toString() + "</u>"));
tvPrivacyUrl.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String url = tvPrivacyUrl.getText().toString().trim();
ToastUtils.show("隐私协议链接:" + url);
}
});
tvPrivacyUrl.setClickable(true);
tvPrivacyUrl.setFocusable(true);
btnAgree.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onPrivacyChangeListener != null) {
onPrivacyChangeListener.onAgreePrivacy();
}
dialog.dismiss();
}
});
btnDisagree.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onPrivacyChangeListener != null) {
onPrivacyChangeListener.onDisagreePrivacy();
}
dialog.dismiss();
}
});
return dialog;
}
}

View File

@@ -56,5 +56,10 @@
</LinearLayout>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>
</LinearLayout>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed May 27 14:51:29 HKT 2026
stageCount=23
#Wed Jun 03 06:52:38 HKT 2026
stageCount=26
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.22
publishVersion=15.20.25
buildCount=0
baseBetaVersion=15.20.23
baseBetaVersion=15.20.26

View File

@@ -263,7 +263,7 @@ public final class CrashHandler {
setContentView(contentView);
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()) + " Error");
}
@Override

View File

@@ -6,6 +6,10 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -125,17 +129,32 @@ public class GlobalApplication extends Application {
*/
@Override
public void onCreate() {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
try {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
restoreDebugStatus();
// 初始化基础组件日志、崩溃处理、Toast
initCoreComponents();
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
restoreDebugStatus();
// 初始化基础组件日志、崩溃处理、Toast
initCoreComponents();
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashHandler.CrashActivity.class
);
}
}
/**
@@ -190,7 +209,7 @@ public class GlobalApplication extends Application {
*/
public static String getAppName(Context context) {
if (context == null) {
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
Log.w(TAG, "getAppName: 上下文为空,返回 null");
return null;
}
PackageManager packageManager = context.getPackageManager();
@@ -206,8 +225,7 @@ public class GlobalApplication extends Application {
return appName;
} catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
Log.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
return null;

1
libgitsion/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

35
libgitsion/build.gradle Normal file
View File

@@ -0,0 +1,35 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply from: '../.winboll/winboll_lib_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 26
targetSdkVersion 30
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.20.15'
api 'cc.winboll.studio:libappbase:15.20.26'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri May 01 17:09:11 HKT 2026
stageCount=57
libraryProject=libdebugtemp
baseVersion=15.0
publishVersion=15.0.56
buildCount=0
baseBetaVersion=15.0.57

17
libgitsion/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# 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 *;
#}

View File

@@ -0,0 +1,27 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libgitsion">
<application>
<service
android:name=".service.GpsSubscribeReceiverService"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="cc.winboll.studio.libgitsion.action.RECEIVE" />
</intent-filter>
</service>
<receiver android:name=".receiver.GpsSubscribeObserverReceiver">
<intent-filter>
<action android:name="cc.winboll.studio.GPS_SUBSCRIBE_CALLBACK"/>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,75 @@
package cc.winboll.studio.libgitsion.manager;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:25
*/
import android.content.Context;
import android.content.Intent;
import java.util.HashMap;
import java.util.Map;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.GpsSubscribeResult;
public final class GpsSubscribeManager {
private static GpsSubscribeManager instance;
private final Map<String,GpsSubscribeMsg> subscribeMap;
private Context appContext;
private GpsSubscribeManager(){
subscribeMap = new HashMap<String, GpsSubscribeMsg>();
}
public static GpsSubscribeManager getInstance(){
if(instance == null){
instance = new GpsSubscribeManager();
}
return instance;
}
public void initContext(final Context context){
this.appContext = context.getApplicationContext();
}
public void addSubscribe(final GpsSubscribeMsg subscribeMsg){
if(subscribeMsg == null){
return;
}
subscribeMap.put(subscribeMsg.getSubscribeUniqueId(),subscribeMsg);
}
public void removeSubscribe(final String sid){
if(sid == null){
return;
}
subscribeMap.remove(sid);
SubscribeLocationManager.getInstance().removeSubscribe(sid);
}
public boolean isSubscribeExist(final String sid){
return subscribeMap.containsKey(sid);
}
public void sendSubscribeResult(final GpsSubscribeResult result){
if(appContext == null || result == null){
return;
}
Intent intent = new Intent(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK);
intent.putExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_RESULT, result);
appContext.sendBroadcast(intent);
}
public void clearAllSubscribe(){
subscribeMap.clear();
SubscribeLocationManager.getInstance().clearAll();
}
public Map<String, GpsSubscribeMsg> getSubscribeMap() {
return subscribeMap;
}
}

View File

@@ -0,0 +1,128 @@
package cc.winboll.studio.libgitsion.manager;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.LocationPoint;
import java.util.HashMap;
import java.util.Map;
public final class SubscribeLocationManager {
private static SubscribeLocationManager instance;
//订阅配置
private final Map<String,GpsSubscribeMsg> subscribeConfigMap;
//基准定点坐标
private final Map<String,LocationPoint> subscriberPointMap;
//真实推送计数(精准统计)
private final Map<String,Integer> subscriberPushCountMap;
private SubscribeLocationManager(){
subscribeConfigMap = new HashMap<String, GpsSubscribeMsg>();
subscriberPointMap = new HashMap<String, LocationPoint>();
subscriberPushCountMap = new HashMap<String, Integer>();
}
public static SubscribeLocationManager getInstance(){
if(instance == null){
instance = new SubscribeLocationManager();
}
return instance;
}
//========= 订阅配置 =========
public void putSubscribeConfig(String sid,GpsSubscribeMsg msg){
subscribeConfigMap.put(sid,msg);
}
public GpsSubscribeMsg getSubscribeConfig(String sid){
return subscribeConfigMap.get(sid);
}
//========= 基准定点坐标 =========
public void initSubscriberPoint(String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public void updateSubscriberPoint(String sid,double lat,double lng){
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
}
public LocationPoint getLastPoint(String sid){
return subscriberPointMap.get(sid);
}
//========= 精准推送计数 =========
public void addPushCount(String sid){
int current = subscriberPushCountMap.get(sid) == null ? 0 : subscriberPushCountMap.get(sid);
subscriberPushCountMap.put(sid,current + 1);
}
public int getPushCount(String sid){
return subscriberPushCountMap.get(sid) == null ? 0 : subscriberPushCountMap.get(sid);
}
public void clearPushCount(String sid){
subscriberPushCountMap.put(sid,0);
}
//========= 步长规则判断 =========
public boolean isNeedPush(String sid,double nowLat,double nowLng){
GpsSubscribeMsg config = getSubscribeConfig(sid);
if(config == null){
return false;
}
//全量订阅直接放行
if(config.getSubscribeMode() == GpsSubscribeConst.SUB_TYPE_ALL){
return true;
}
//无初始定点 → 先建立第一个基准点
LocationPoint lastPoint = getLastPoint(sid);
if(lastPoint == null){
return true;
}
//计算实际移动距离
double distance = calculateDistance(
lastPoint.getLatitude(),lastPoint.getLongitude(),
nowLat,nowLng
);
return distance >= config.getStepDistanceM();
}
//两点经纬度距离计算(米)
private double calculateDistance(double lat1,double lng1,double lat2,double lng2){
double radLat1 = Math.toRadians(lat1);
double radLat2 = Math.toRadians(lat2);
double radLng1 = Math.toRadians(lng1);
double radLng2 = Math.toRadians(lng2);
double latDiff = radLat1 - radLat2;
double lngDiff = radLng1 - radLng2;
double value = 2 * Math.asin(Math.sqrt(
Math.pow(Math.sin(latDiff / 2),2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(lngDiff / 2),2)
));
return value * 6378137;
}
//========= 移除 & 清空 =========
public void removeSubscribe(String sid){
subscribeConfigMap.remove(sid);
subscriberPointMap.remove(sid);
subscriberPushCountMap.remove(sid);
}
public void clearAll(){
subscribeConfigMap.clear();
subscriberPointMap.clear();
subscriberPushCountMap.clear();
}
}

View File

@@ -0,0 +1,51 @@
package cc.winboll.studio.libgitsion.model;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:22
* WinBoLL Studio
* Java7 | API26-30
*/
public final class GpsSubscribeConst {
// 新增GPS定位推送广播
public static final String ACTION_GPS_LOCATION = "cc.winboll.studio.ACTION_GPS_LOCATION";
//订阅运行模式
public static final int SUB_TYPE_ALL = 1;
public static final int SUB_TYPE_STEP_DISTANCE = 2;
//原始数据订阅类型
public static final int SUBSCRIBE_TYPE_LOCATION = 1;
public static final int SUBSCRIBE_TYPE_SATELLITE = 2;
public static final int SUBSCRIBE_TYPE_NMEA = 3;
//订阅返回码
public static final int RESULT_SUCCESS = 0;
public static final int RESULT_PERMISSION_DENY = 1;
public static final int RESULT_PARAM_ERROR = 2;
public static final int RESULT_GPS_NOT_AVAILABLE = 3;
public static final int RESULT_SYSTEM_LIMIT = 4;
//GPS设备状态
public static final int GPS_STATE_CLOSE = 0;
public static final int GPS_STATE_SCANNING = 1;
public static final int GPS_STATE_LOCATED = 2;
public static final int GPS_STATE_SIGNAL_WEAK = 3;
//广播Action
public static final String ACTION_SUBSCRIBE_REQUEST = "cc.winboll.studio.GPS_SUBSCRIBE_REQUEST";
public static final String ACTION_SUBSCRIBE_CALLBACK = "cc.winboll.studio.GPS_SUBSCRIBE_CALLBACK";
//Intent Extra 键名
public static final String EXTRA_SUBSCRIBE_SID = "extra_subscribe_sid";
public static final String EXTRA_SUBSCRIBE_RESULT = "extra_subscribe_result";
public static final String EXTRA_LOCATION_POINT = "extra_location_point";
//超时毫秒
public static final long SUBSCRIBE_TIME_OUT = 5000;
//地球半径 距离计算常量
public static final double EARTH_RADIUS = 6378137.0;
}

View File

@@ -0,0 +1,137 @@
package cc.winboll.studio.libgitsion.model;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:24
*/
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
public final class GpsSubscribeMsg implements Parcelable {
private final String subscribePackage;
private final int subscribeMode;
private final float stepDistanceM;
private final int subscribeType;
private final long updateInterval;
private final float minDistance;
private final boolean backgroundPush;
private final String subscribeUniqueId;
public GpsSubscribeMsg(String subscribePackage,
int subscribeMode,
float stepDistanceM,
int subscribeType,
long updateInterval,
float minDistance,
boolean backgroundPush,
String subscribeUniqueId) {
this.subscribePackage = subscribePackage;
this.subscribeMode = subscribeMode;
this.stepDistanceM = stepDistanceM;
this.subscribeType = subscribeType;
this.updateInterval = updateInterval;
this.minDistance = minDistance;
this.backgroundPush = backgroundPush;
this.subscribeUniqueId = subscribeUniqueId;
}
public String getSubscribePackage() {
return subscribePackage;
}
public int getSubscribeMode() {
return subscribeMode;
}
public float getStepDistanceM() {
return stepDistanceM;
}
public int getSubscribeType() {
return subscribeType;
}
public long getUpdateInterval() {
return updateInterval;
}
public float getMinDistance() {
return minDistance;
}
public boolean isBackgroundPush() {
return backgroundPush;
}
public String getSubscribeUniqueId() {
return subscribeUniqueId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(subscribePackage);
dest.writeInt(subscribeMode);
dest.writeFloat(stepDistanceM);
dest.writeInt(subscribeType);
dest.writeLong(updateInterval);
dest.writeFloat(minDistance);
dest.writeByte((byte) (backgroundPush ? 1 : 0));
dest.writeString(subscribeUniqueId);
}
public static final Creator<GpsSubscribeMsg> CREATOR = new Creator<GpsSubscribeMsg>() {
@Override
public GpsSubscribeMsg createFromParcel(Parcel in) {
return new GpsSubscribeMsg(
in.readString(),
in.readInt(),
in.readFloat(),
in.readInt(),
in.readLong(),
in.readFloat(),
in.readByte() == 1,
in.readString()
);
}
@Override
public GpsSubscribeMsg[] newArray(int size) {
return new GpsSubscribeMsg[size];
}
};
public Bundle convertToBundle() {
Bundle bundle = new Bundle();
bundle.putString("pkg", subscribePackage);
bundle.putInt("subMode",subscribeMode);
bundle.putFloat("stepM",stepDistanceM);
bundle.putInt("type", subscribeType);
bundle.putLong("interval", updateInterval);
bundle.putFloat("distance", minDistance);
bundle.putBoolean("bgPush", backgroundPush);
bundle.putString("sid", subscribeUniqueId);
return bundle;
}
public static GpsSubscribeMsg createByBundle(Bundle bundle) {
return new GpsSubscribeMsg(
bundle.getString("pkg"),
bundle.getInt("subMode"),
bundle.getFloat("stepM"),
bundle.getInt("type"),
bundle.getLong("interval"),
bundle.getFloat("distance"),
bundle.getBoolean("bgPush"),
bundle.getString("sid")
);
}
}

View File

@@ -0,0 +1,148 @@
package cc.winboll.studio.libgitsion.model;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:25
*/
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
public final class GpsSubscribeResult implements Parcelable {
private final String subscribeUniqueId;
private final int resultCode;
private final String resultDesc;
private final int gpsRunningState;
private final long realEffectiveInterval;
private final long currentTimeStamp;
private final double latitude;
private final double longitude;
private final long locationTime;
public GpsSubscribeResult(String subscribeUniqueId,
int resultCode,
String resultDesc,
int gpsRunningState,
long realEffectiveInterval,
long currentTimeStamp,
double latitude,
double longitude,
long locationTime) {
this.subscribeUniqueId = subscribeUniqueId;
this.resultCode = resultCode;
this.resultDesc = resultDesc;
this.gpsRunningState = gpsRunningState;
this.realEffectiveInterval = realEffectiveInterval;
this.currentTimeStamp = currentTimeStamp;
this.latitude = latitude;
this.longitude = longitude;
this.locationTime = locationTime;
}
public String getSubscribeUniqueId() {
return subscribeUniqueId;
}
public int getResultCode() {
return resultCode;
}
public String getResultDesc() {
return resultDesc;
}
public int getGpsRunningState() {
return gpsRunningState;
}
public long getRealEffectiveInterval() {
return realEffectiveInterval;
}
public long getCurrentTimeStamp() {
return currentTimeStamp;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public long getLocationTime() {
return locationTime;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(subscribeUniqueId);
dest.writeInt(resultCode);
dest.writeString(resultDesc);
dest.writeInt(gpsRunningState);
dest.writeLong(realEffectiveInterval);
dest.writeLong(currentTimeStamp);
dest.writeDouble(latitude);
dest.writeDouble(longitude);
dest.writeLong(locationTime);
}
public static final Creator<GpsSubscribeResult> CREATOR = new Creator<GpsSubscribeResult>() {
@Override
public GpsSubscribeResult createFromParcel(Parcel in) {
return new GpsSubscribeResult(
in.readString(),
in.readInt(),
in.readString(),
in.readInt(),
in.readLong(),
in.readLong(),
in.readDouble(),
in.readDouble(),
in.readLong()
);
}
@Override
public GpsSubscribeResult[] newArray(int size) {
return new GpsSubscribeResult[size];
}
};
public Bundle convertToBundle() {
Bundle bundle = new Bundle();
bundle.putString("sid", subscribeUniqueId);
bundle.putInt("code", resultCode);
bundle.putString("desc", resultDesc);
bundle.putInt("gpsState", gpsRunningState);
bundle.putLong("realInterval", realEffectiveInterval);
bundle.putLong("time", currentTimeStamp);
bundle.putDouble("lat", latitude);
bundle.putDouble("lng", longitude);
bundle.putLong("locTime", locationTime);
return bundle;
}
public static GpsSubscribeResult createByBundle(Bundle bundle) {
return new GpsSubscribeResult(
bundle.getString("sid"),
bundle.getInt("code"),
bundle.getString("desc"),
bundle.getInt("gpsState"),
bundle.getLong("realInterval"),
bundle.getLong("time"),
bundle.getDouble("lat"),
bundle.getDouble("lng"),
bundle.getLong("locTime")
);
}
}

View File

@@ -0,0 +1,37 @@
package cc.winboll.studio.libgitsion.model;
import java.io.Serializable;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:23
* 订阅者基准定点坐标
* 每次推送成功自动更新
*/
public final class LocationPoint implements Serializable {
private static final long serialVersionUID = 1L;
private final double latitude;
private final double longitude;
private final long recordTime;
public LocationPoint(double latitude, double longitude, long recordTime) {
this.latitude = latitude;
this.longitude = longitude;
this.recordTime = recordTime;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public long getRecordTime() {
return recordTime;
}
}

View File

@@ -0,0 +1,42 @@
package cc.winboll.studio.libgitsion.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:27
*/
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeResult;
public final class GpsSubscribeObserverReceiver extends BroadcastReceiver {
private OnSubscribeResultListener listener;
public void setOnSubscribeResultListener(OnSubscribeResultListener listener){
this.listener = listener;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK.equals(action)){
GpsSubscribeResult result = intent.getParcelableExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_RESULT);
if(listener != null && result != null){
listener.onResultBack(result);
}
}
}
public interface OnSubscribeResultListener{
void onResultBack(GpsSubscribeResult result);
}
}

View File

@@ -0,0 +1,94 @@
package cc.winboll.studio.libgitsion.service;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libgitsion.manager.SubscribeLocationManager;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.GpsSubscribeResult;
import cc.winboll.studio.libgitsion.model.LocationPoint;
/**
* 全局消息接收父类服务
* 所有应用内接收服务全部继承此类
*/
public abstract class GpsSubscribeReceiverService extends Service {
public static final String TAG_PARENT = "GpsSubscribeReceiverService";
//当前绑定的视图订阅SID
protected String bindViewSid;
private BroadcastReceiver mCallbackReceiver;
public void bindControlSid(String sid){
this.bindViewSid = sid;
}
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG_PARENT, "Service onCreate, 注册广播接收器");
mCallbackReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK.equals(intent.getAction())) {
GpsSubscribeResult result = intent.getParcelableExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_RESULT);
if (result != null && bindViewSid != null
&& bindViewSid.equals(result.getSubscribeUniqueId())) {
LocationPoint point = new LocationPoint(
result.getLatitude(),
result.getLongitude(),
result.getLocationTime()
);
GpsSubscribeMsg config = SubscribeLocationManager.getInstance()
.getSubscribeConfig(bindViewSid);
LogUtils.d(TAG_PARENT, "收到GPS推送转发至 onReceiveGpsDataSID" + bindViewSid);
onReceiveGpsData(point, config);
}
}
}
};
registerReceiver(mCallbackReceiver, new IntentFilter(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.hasExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_SID)) {
String sid = intent.getStringExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_SID);
bindControlSid(sid);
LogUtils.d(TAG_PARENT, "绑定SID" + sid);
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCallbackReceiver != null) {
unregisterReceiver(mCallbackReceiver);
mCallbackReceiver = null;
LogUtils.d(TAG_PARENT, "广播接收器已注销");
}
}
/**
* 统一接收GPS推送入口
*/
public void onReceiveGpsData(LocationPoint point, GpsSubscribeMsg config){
//父类统一日志溯源
LogUtils.d(TAG_PARENT,"【消息溯源】接收视图SID" + bindViewSid);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@@ -0,0 +1,51 @@
package cc.winboll.studio.libgitsion.util;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/05/07 10:26
*/
import android.os.Handler;
import android.os.Message;
public final class TimeCountUtil {
private final Handler mHandler;
private long totalTime;
private boolean isRunning;
public static final int COUNT_FINISH = 1001;
public TimeCountUtil(final OnCountListener listener) {
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what == COUNT_FINISH){
isRunning = false;
if(listener != null){
listener.onTimeOut();
}
}
}
};
}
public void start(long time){
if(isRunning){
return;
}
totalTime = time;
isRunning = true;
mHandler.sendEmptyMessageDelayed(COUNT_FINISH,totalTime);
}
public void cancel(){
mHandler.removeMessages(COUNT_FINISH);
isRunning = false;
}
public interface OnCountListener{
void onTimeOut();
}
}

View File

@@ -0,0 +1,227 @@
package cc.winboll.studio.libgitsion.view;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libgitsion.R;
import cc.winboll.studio.libgitsion.manager.GpsSubscribeManager;
import cc.winboll.studio.libgitsion.manager.SubscribeLocationManager;
import cc.winboll.studio.libgitsion.model.GpsSubscribeConst;
import cc.winboll.studio.libgitsion.model.GpsSubscribeMsg;
import cc.winboll.studio.libgitsion.model.LocationPoint;
import java.util.UUID;
public final class GpsSubscribeControlView extends LinearLayout {
//常量抽取
private static final long REFRESH_INTERVAL = 600;
private RadioGroup rgSubscribeMode;
private RadioButton rbModeAll;
private RadioButton rbModeStep;
private EditText etStepMeter;
private Switch switchSubscribe;
private TextView tvSubscribeSid;
private TextView tvSubscribeRecord;
private String currentSubscribeSid;
//一对一专属绑定的接收服务
private Class<?> mBindReceiverServiceClazz;
//final管理器 构造器初始化
private final GpsSubscribeManager mSubscribeManager;
private final SubscribeLocationManager mLocationManager;
private final Handler mRefreshHandler = new Handler(Looper.getMainLooper());
public GpsSubscribeControlView(Context context) {
super(context);
mSubscribeManager = GpsSubscribeManager.getInstance();
mLocationManager = SubscribeLocationManager.getInstance();
initView(context);
}
public GpsSubscribeControlView(Context context, AttributeSet attrs) {
super(context, attrs);
mSubscribeManager = GpsSubscribeManager.getInstance();
mLocationManager = SubscribeLocationManager.getInstance();
initView(context);
}
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.view_gps_subscribe_control, this, true);
rgSubscribeMode = findViewById(R.id.rg_subscribe_mode);
rbModeAll = findViewById(R.id.rb_mode_all);
rbModeStep = findViewById(R.id.rb_mode_step);
etStepMeter = findViewById(R.id.et_step_meter);
switchSubscribe = findViewById(R.id.switch_subscribe);
tvSubscribeSid = findViewById(R.id.tv_subscribe_sid);
tvSubscribeRecord = findViewById(R.id.tv_subscribe_record);
initDefaultConfig();
initModeSwitch();
initSubscribeSwitch();
startAutoRefreshRecord();
}
private void initDefaultConfig() {
currentSubscribeSid = UUID.randomUUID().toString().substring(0, 16);
tvSubscribeSid.setText("订阅SID" + currentSubscribeSid);
rbModeAll.setChecked(true);
etStepMeter.setText("10");
}
/**
* 外部绑定当前视图专属的接收服务Class
*/
public void bindReceiverService(Class<?> serviceClazz){
this.mBindReceiverServiceClazz = serviceClazz;
}
private void initModeSwitch() {
rgSubscribeMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
etStepMeter.setVisibility(checkedId == R.id.rb_mode_step ? VISIBLE : GONE);
}
});
}
private void initSubscribeSwitch() {
switchSubscribe.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
startSubscribe();
} else {
stopSubscribe();
}
}
});
}
private void startSubscribe() {
int subMode = GpsSubscribeConst.SUB_TYPE_ALL;
float stepVal = 10f;
if (rbModeStep.isChecked()) {
subMode = GpsSubscribeConst.SUB_TYPE_STEP_DISTANCE;
try {
stepVal = Float.parseFloat(etStepMeter.getText().toString().trim());
} catch (Exception ignored) {}
}
GpsSubscribeMsg subscribeMsg = new GpsSubscribeMsg(
getContext().getPackageName(),
subMode,
stepVal,
GpsSubscribeConst.SUBSCRIBE_TYPE_LOCATION,
1000,
1f,
true,
currentSubscribeSid
);
mSubscribeManager.addSubscribe(subscribeMsg);
mLocationManager.putSubscribeConfig(currentSubscribeSid, subscribeMsg);
mLocationManager.clearPushCount(currentSubscribeSid);
//开启订阅自动启动专属接收服务携带SID
if(mBindReceiverServiceClazz != null){
Intent startServiceIntent = new Intent(getContext(), mBindReceiverServiceClazz);
startServiceIntent.putExtra(GpsSubscribeConst.EXTRA_SUBSCRIBE_SID, currentSubscribeSid);
getContext().startService(startServiceIntent);
}
}
private void stopSubscribe() {
mSubscribeManager.removeSubscribe(currentSubscribeSid);
mLocationManager.removeSubscribe(currentSubscribeSid);
tvSubscribeRecord.setText("状态:未订阅");
//关闭订阅 同步停止专属接收服务
if(mBindReceiverServiceClazz != null){
Intent stopServiceIntent = new Intent(getContext(), mBindReceiverServiceClazz);
getContext().stopService(stopServiceIntent);
}
}
private void startAutoRefreshRecord() {
mRefreshHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshRecordInfo();
mRefreshHandler.postDelayed(this, REFRESH_INTERVAL);
}
}, REFRESH_INTERVAL);
}
private void refreshRecordInfo() {
if (!switchSubscribe.isChecked()) {
tvSubscribeRecord.setText("状态:空闲未订阅");
return;
}
GpsSubscribeMsg config = mLocationManager.getSubscribeConfig(currentSubscribeSid);
LocationPoint lastPoint = mLocationManager.getLastPoint(currentSubscribeSid);
if (config == null) {
tvSubscribeRecord.setText("状态:已订阅|等待管理器加载");
return;
}
String modeText = config.getSubscribeMode() == GpsSubscribeConst.SUB_TYPE_ALL
? "全量订阅" : "步长订阅";
int realPushCount = mLocationManager.getPushCount(currentSubscribeSid);
StringBuilder record = new StringBuilder();
record.append("【订阅实时数据表】\n");
record.append("订阅模式:").append(modeText).append("\n");
record.append("步长阈值:").append(config.getStepDistanceM()).append("\n");
if(lastPoint != null){
record.append("基准定点:").append(lastPoint.getLatitude()).append(" , ").append(lastPoint.getLongitude()).append("\n");
}else{
record.append("基准定点:等待首次定位建立\n");
}
record.append("真实推送次数:").append(realPushCount).append("");
tvSubscribeRecord.setText(record);
}
public String getCurrentSid() {
return currentSubscribeSid;
}
public boolean isSubscribeOpen() {
return switchSubscribe.isChecked();
}
/**
* 视图销毁:强制停止订阅 + 停止服务 + 清空刷新任务
*/
@Override
protected void onDetachedFromWindow() {
if(switchSubscribe.isChecked()){
switchSubscribe.setChecked(false);
}
mRefreshHandler.removeCallbacksAndMessages(null);
super.onDetachedFromWindow();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="14dp"
android:layout_marginVertical="6dp"
android:background="#282828"
android:clipToPadding="false">
<!-- SID标识 -->
<TextView
android:id="@+id/tv_subscribe_sid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#999999"
android:textSize="11sp"/>
<!-- 订阅模式选择 -->
<RadioGroup
android:id="@+id/rg_subscribe_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/rb_mode_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="全量订阅"
android:textColor="#FFFFFF"/>
<RadioButton
android:id="@+id/rb_mode_step"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="步长订阅"
android:layout_marginStart="18dp"
android:textColor="#FFFFFF"/>
</RadioGroup>
<!-- 步长输入 -->
<EditText
android:id="@+id/et_step_meter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="移动阈值(米)"
android:inputType="numberDecimal"
android:text="10"
android:visibility="gone"
android:textColor="#ffffff"
android:textColorHint="#666666"
android:layout_marginTop="8dp"/>
<!-- 订阅开关 -->
<Switch
android:id="@+id/switch_subscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开启独立订阅"
android:textColor="#EEEEEE"
android:layout_marginTop="10dp"/>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#444444"
android:layout_marginTop="12dp"/>
<!-- 订阅数据记录表【ID完全对应源码 tv_subscribe_record】 -->
<TextView
android:id="@+id/tv_subscribe_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#88EE88"
android:textSize="10sp"
android:layout_marginTop="10dp"
android:gravity="start"/>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="@android:style/Theme.Material.Light">
</style>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lib_name">libdebugtemp</string>
<string name="hello_world">Hello world!</string>
<string name="text_libraryactivity">LibraryActivity</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="@android:style/Theme.Holo.Light">
</style>
</resources>

1
libwinboll/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

55
libwinboll/build.gradle Normal file
View File

@@ -0,0 +1,55 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply from: '../.winboll/winboll_lib_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 26
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// Gson
api 'com.google.code.gson:gson:2.8.9'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 添加JSch依赖SFTP核心com.jcraft:jsch:0.1.54
api 'com.jcraft:jsch:0.1.54'
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libappbase:15.20.22'
//api 'cc.winboll.studio:libaes:15.20.12'
// 备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.22'
api 'com.github.ZhanGSKen:libaes:aes-v15.20.12'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 02 20:00:52 HKT 2026
stageCount=7
libraryProject=libwinboll
baseVersion=15.20
publishVersion=15.20.6
buildCount=0
baseBetaVersion=15.20.7

17
libwinboll/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# 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 *;
#}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libwinboll" >
<application>
<activity
android:name=".WinBoLLLibraryActivity">
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,17 @@
package cc.winboll.studio.libwinboll;
import android.app.Activity;
import android.os.Bundle;
import cc.winboll.studio.libappbase.ToastUtils;
public class WinBoLLLibraryActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_winbolllibrary);
ToastUtils.show("WinBoLLLibraryActivity onCreate");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Some files were not shown because too many files have changed in this diff Show More