Compare commits
15 Commits
appbase
...
89f96a7b99
| Author | SHA1 | Date | |
|---|---|---|---|
| 89f96a7b99 | |||
| 429db23050 | |||
| 3e4a64f31e | |||
| 2927303a88 | |||
| 2c4fc218b0 | |||
| b065a20c4d | |||
| b3df8c7770 | |||
| dae269ff77 | |||
| cb8c3448f5 | |||
| 0e90f40f0f | |||
| 11aee7e373 | |||
| 58a93a6746 | |||
| 38eacb9a57 | |||
| 377d084aad | |||
| a16d98cad0 |
117
gpsrelaysentinel/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# GPSRelaySentinel
|
||||||
|
|
||||||
|
## 介绍
|
||||||
|
|
||||||
|
GPSRelaySentinel 是一款基于安卓平台的综合工具应用,集成 Termux 终端模拟器、二维码扫描、网络请求等功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **编程语言**: Java 7(源码)
|
||||||
|
- **编译环境**: Java 11(Gradle 编译)
|
||||||
|
- **Gradle 插件**: 7.2.1
|
||||||
|
- **安卓 API**:
|
||||||
|
- 最低支持: API 26 (Android 8.0)
|
||||||
|
- 目标版本: API 30 (Android 11)
|
||||||
|
- 编译版本: API 30
|
||||||
|
|
||||||
|
## 软件架构
|
||||||
|
|
||||||
|
适配以下安卓开发环境的 Gradle 编译结构:
|
||||||
|
- AIDE Pro
|
||||||
|
- AndroidIDE
|
||||||
|
|
||||||
|
## 模块说明
|
||||||
|
|
||||||
|
本项目采用多模块结构:
|
||||||
|
- `gpsrelaysentinel` - 主应用模块
|
||||||
|
- `libappbase` - 基础库模块(提供 OkHttp、Gson、JSch 等基础能力)
|
||||||
|
- `libaes` - AES 加密库模块(提供权限请求、二维码、拼音搜索等扩展功能)
|
||||||
|
|
||||||
|
## 核心依赖库
|
||||||
|
|
||||||
|
### 网络相关
|
||||||
|
- 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 - 下拉刷新
|
||||||
|
|
||||||
|
## Gradle 编译说明
|
||||||
|
|
||||||
|
### 调试版编译
|
||||||
|
```bash
|
||||||
|
gradle assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段版编译(发布)
|
||||||
|
```bash
|
||||||
|
bash .winboll/bashPublishAPKAddTag.sh gpsrelaysentinel
|
||||||
|
```
|
||||||
|
|
||||||
|
### 版本管理
|
||||||
|
版本信息由 `gpsrelaysentinel/build.properties` 管理:
|
||||||
|
- `baseVersion` - 基础版本号
|
||||||
|
- `stageCount` - 阶段构建次数
|
||||||
|
- `publishVersion` - 发布版本号
|
||||||
|
- `buildCount` - 构建次数
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### Termux 应用配置
|
||||||
|
|
||||||
|
1. 安装 Termux 应用(包名: `com.termux`)
|
||||||
|
2. 配置允许外部应用访问:
|
||||||
|
```bash
|
||||||
|
echo "allow-external-apps = true" > ~/.termux/termux.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限说明
|
||||||
|
|
||||||
|
应用需要以下权限:
|
||||||
|
- 网络访问权限
|
||||||
|
- 存储读写权限
|
||||||
|
- 相机权限(二维码扫描)
|
||||||
|
- 位置权限(GPS 相关功能)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
gpsrelaysentinel/
|
||||||
|
├── src/main/
|
||||||
|
│ ├── java/ # Java 源码(Java 7 语法)
|
||||||
|
│ ├── res/ # 资源文件
|
||||||
|
│ ├── libs/ # 本地库文件(含 JNI 库)
|
||||||
|
│ └── AndroidManifest.xml
|
||||||
|
├── build.gradle # 模块构建配置
|
||||||
|
└── build.properties # 版本配置文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参与贡献
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 新建功能分支 (`git checkout -b feat_xxx`)
|
||||||
|
3. 提交代码(作者: ZhanGSKen <zhangsken@188.com>)
|
||||||
|
4. 新建 Pull Request
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
[待添加许可证信息]
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [Android Developer Documentation](https://developer.android.com/)
|
||||||
|
- [Termux Wiki](https://wiki.termux.com/)
|
||||||
|
- [Gradle User Manual](https://docs.gradle.org/)
|
||||||
1
gpsrelaysentinel/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
126
gpsrelaysentinel/build.gradle
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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.gpsrelaysentinel"
|
||||||
|
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(':libgpsrelaysentinel')
|
||||||
|
|
||||||
|
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.15.2'
|
||||||
|
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||||
|
|
||||||
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
|
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||||
|
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||||
|
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
8
gpsrelaysentinel/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Thu May 07 10:59:47 CST 2026
|
||||||
|
stageCount=27
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.11
|
||||||
|
publishVersion=15.11.26
|
||||||
|
buildCount=31
|
||||||
|
baseBetaVersion=15.11.27
|
||||||
137
gpsrelaysentinel/proguard-rules.pro
vendored
Normal 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
|
||||||
|
|
||||||
12
gpsrelaysentinel/src/beta/AndroidManifest.xml
Normal 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>
|
||||||
|
|
||||||
6
gpsrelaysentinel/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">GPSRelaySentinel★</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
46
gpsrelaysentinel/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.gpsrelaysentinel">
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
package cc.winboll.studio.gpsrelaysentinel;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// 初始化 Toast 框架
|
||||||
|
ToastUtils.init(this);
|
||||||
|
|
||||||
|
//CrashHandler.getInstance().registerGlobal(this);
|
||||||
|
//CrashHandler.getInstance().registerPart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||||
|
byte[] buf = new byte[1024 * 8];
|
||||||
|
int len;
|
||||||
|
while ((len = input.read(buf)) != -1) {
|
||||||
|
output.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(File file, byte[] data) throws IOException {
|
||||||
|
File parent = file.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||||
|
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||||
|
FileOutputStream output = new FileOutputStream(file);
|
||||||
|
try {
|
||||||
|
write(input, output);
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toString(InputStream input) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
write(input, output);
|
||||||
|
try {
|
||||||
|
return output.toString("UTF-8");
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeIO(Closeable... closeables) {
|
||||||
|
for (Closeable closeable : closeables) {
|
||||||
|
try {
|
||||||
|
if (closeable != null) closeable.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CrashHandler {
|
||||||
|
|
||||||
|
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
|
||||||
|
private static CrashHandler sInstance;
|
||||||
|
|
||||||
|
private PartCrashHandler mPartCrashHandler;
|
||||||
|
|
||||||
|
public static CrashHandler getInstance() {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new CrashHandler();
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context) {
|
||||||
|
registerGlobal(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context, String crashDir) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister() {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPart(Context context) {
|
||||||
|
unregisterPart(context);
|
||||||
|
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||||
|
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterPart(Context context) {
|
||||||
|
if (mPartCrashHandler != null) {
|
||||||
|
mPartCrashHandler.isRunning.set(false);
|
||||||
|
mPartCrashHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PartCrashHandler implements Runnable {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
public PartCrashHandler(Context context) {
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (isRunning.get()) {
|
||||||
|
try {
|
||||||
|
Looper.loop();
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
if (isRunning.get()) {
|
||||||
|
MAIN_HANDLER.post(new Runnable(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (e instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException)e;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
private final File mCrashDir;
|
||||||
|
|
||||||
|
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||||
|
this.mContext = context;
|
||||||
|
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
String log = buildLog(throwable);
|
||||||
|
writeLog(log);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
writeLog(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
throwable.printStackTrace();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLog(Throwable throwable) {
|
||||||
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
|
||||||
|
String versionName = "unknown";
|
||||||
|
long versionCode = 0;
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||||
|
versionName = packageInfo.versionName;
|
||||||
|
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
|
||||||
|
LinkedHashMap<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package cc.winboll.studio.gpsrelaysentinel;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.LogView;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
LogView mLogView;
|
||||||
|
Switch mSwitchService;
|
||||||
|
private static final int REQUEST_LOCATION_PERMISSION = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
mLogView = findViewById(R.id.logview);
|
||||||
|
mSwitchService = findViewById(R.id.switch_service);
|
||||||
|
|
||||||
|
// 根据当前权限状态初始化switch
|
||||||
|
mSwitchService.setChecked(hasLocationPermission());
|
||||||
|
|
||||||
|
mSwitchService.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
if (isChecked) {
|
||||||
|
// 打开时检查权限
|
||||||
|
if (hasLocationPermission()) {
|
||||||
|
startService();
|
||||||
|
} else {
|
||||||
|
// 没有权限,申请权限
|
||||||
|
requestLocationPermission();
|
||||||
|
// 暂时不打开switch,等权限申请结果
|
||||||
|
mSwitchService.setChecked(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ToastUtils.show("onCreate");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasLocationPermission() {
|
||||||
|
boolean hasBasic = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|| checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
if (hasBasic && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
return checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
return hasBasic;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestLocationPermission() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
String[] permissions = new String[] {
|
||||||
|
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
android.Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
|
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
||||||
|
};
|
||||||
|
requestPermissions(permissions, REQUEST_LOCATION_PERMISSION);
|
||||||
|
} else {
|
||||||
|
String[] permissions = new String[] {
|
||||||
|
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
};
|
||||||
|
requestPermissions(permissions, REQUEST_LOCATION_PERMISSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startService() {
|
||||||
|
Intent intent = new Intent(MainActivity.this, MainService.class);
|
||||||
|
startForegroundService(intent);
|
||||||
|
ToastUtils.show("GPS Service started");
|
||||||
|
LogUtils.d(MainService.TAG, "GPS Service started from MainActivity");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopService() {
|
||||||
|
// 先设置SP标记为不启用
|
||||||
|
MainActivity.this.getSharedPreferences(MainService.PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(MainService.KEY_SERVICE_ENABLED, false)
|
||||||
|
.apply();
|
||||||
|
Intent intent = new Intent(MainActivity.this, MainService.class);
|
||||||
|
stopService(intent);
|
||||||
|
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);
|
||||||
|
startService();
|
||||||
|
} else {
|
||||||
|
ToastUtils.show("需要位置权限才能使用GPS服务");
|
||||||
|
mSwitchService.setChecked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
mLogView.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// public void onLibraryActivity(View view) {
|
||||||
|
// startActivity(new Intent(this, LibraryActivity.class));
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package cc.winboll.studio.gpsrelaysentinel;
|
||||||
|
|
||||||
|
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.libgpsrelaysentinel.manager.GpsSubscribeManager;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.manager.SubscribeLocationManager;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MainService extends Service {
|
||||||
|
|
||||||
|
public static final String TAG = "MainService";
|
||||||
|
private LocationManager mLocationManager;
|
||||||
|
private LocationListener mLocationListener;
|
||||||
|
private boolean mIsRunning = false;
|
||||||
|
private NotificationManager mNotificationManager;
|
||||||
|
private NotificationCompat.Builder mNotificationBuilder;
|
||||||
|
private static final String CHANNEL_ID = "gps_relay_channel";
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
static final String PREF_NAME = "gps_relay_service_prefs";
|
||||||
|
static final String KEY_SERVICE_ENABLED = "service_enabled";
|
||||||
|
private int mGpsCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
LogUtils.d(TAG, "Service onCreate");
|
||||||
|
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
createNotificationChannel();
|
||||||
|
if (isServiceEnabled()) {
|
||||||
|
LogUtils.d(TAG, "Service was enabled, starting GPS updates");
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
LogUtils.d(TAG, "Service onStartCommand");
|
||||||
|
setServiceEnabled(true);
|
||||||
|
run();
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isServiceEnabled() {
|
||||||
|
return getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).getBoolean(KEY_SERVICE_ENABLED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setServiceEnabled(boolean enabled) {
|
||||||
|
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit().putBoolean(KEY_SERVICE_ENABLED, enabled).apply();
|
||||||
|
LogUtils.d(TAG, "Service enabled set to: " + enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
if (mIsRunning) {
|
||||||
|
LogUtils.d(TAG, "GPS updates already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
mLocationListener = new LocationListener() {
|
||||||
|
@Override
|
||||||
|
public void onLocationChanged(Location location) {
|
||||||
|
mGpsCount++;
|
||||||
|
String gpsInfo = "Lat: " + location.getLatitude() + ", Lng: " + location.getLongitude();
|
||||||
|
LogUtils.d(TAG, "Location changed: " + gpsInfo);
|
||||||
|
updateNotification(gpsInfo);
|
||||||
|
|
||||||
|
//管理器初始化
|
||||||
|
GpsSubscribeManager subscribeManager = GpsSubscribeManager.getInstance();
|
||||||
|
SubscribeLocationManager locationManager = SubscribeLocationManager.getInstance();
|
||||||
|
|
||||||
|
//遍历所有订阅者做距离判断+定点更新
|
||||||
|
Map<String, GpsSubscribeMsg> subscribeMap = subscribeManager.getSubscribeMap();
|
||||||
|
for (Map.Entry<String, GpsSubscribeMsg> entry : subscribeMap.entrySet()) {
|
||||||
|
String sid = entry.getKey();
|
||||||
|
GpsSubscribeMsg subscribeMsg = entry.getValue();
|
||||||
|
|
||||||
|
double nowLat = location.getLatitude();
|
||||||
|
double nowLng = location.getLongitude();
|
||||||
|
|
||||||
|
//判断是否满足推送
|
||||||
|
boolean canPush = locationManager.isNeedPush(sid, nowLat, nowLng);
|
||||||
|
if (canPush) {
|
||||||
|
//执行发送GPS广播
|
||||||
|
//sendGpsBroadcast(...);
|
||||||
|
|
||||||
|
//推送成功立刻刷新订阅者基准坐标
|
||||||
|
locationManager.updateSubscriberPoint(sid, nowLat, nowLng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||||
|
LogUtils.d(TAG, "Status changed: " + provider + ", status: " + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderEnabled(String provider) {
|
||||||
|
LogUtils.d(TAG, "Provider enabled: " + provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderDisabled(String provider) {
|
||||||
|
LogUtils.d(TAG, "Provider disabled: " + provider);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||||
|
mLocationManager.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
1000,
|
||||||
|
1,
|
||||||
|
mLocationListener
|
||||||
|
);
|
||||||
|
LogUtils.d(TAG, "GPS location updates requested");
|
||||||
|
mIsRunning = true;
|
||||||
|
startForegroundNotification();
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
LogUtils.e(TAG, "Permission denied: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"GPS Relay Service",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
);
|
||||||
|
channel.setDescription("GPS Relay Sentinel service channel");
|
||||||
|
mNotificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startForegroundNotification() {
|
||||||
|
mNotificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("GPS Relay Service")
|
||||||
|
.setContentText("Waiting for GPS data...")
|
||||||
|
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||||
|
.setOngoing(true);
|
||||||
|
Notification notification = mNotificationBuilder.build();
|
||||||
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotification(String gpsInfo) {
|
||||||
|
if (mNotificationBuilder != null) {
|
||||||
|
mNotificationBuilder.setContentText(gpsInfo + " | Count: " + mGpsCount);
|
||||||
|
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, "Permission denied when removing updates: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mIsRunning = false;
|
||||||
|
LogUtils.d(TAG, "Service onDestroy");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
65
gpsrelaysentinel/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical|center_horizontal"
|
||||||
|
android:id="@+id/ll_main">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="GPSRelaySentinel"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_service"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="GPS Service"
|
||||||
|
android:checked="false"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<cc.winboll.studio.libappbase.LogView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/logview"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
BIN
gpsrelaysentinel/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
gpsrelaysentinel/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
6
gpsrelaysentinel/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#009688</color>
|
||||||
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
|
<color name="colorAccent">#FF9800</color>
|
||||||
|
</resources>
|
||||||
4
gpsrelaysentinel/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">GPSRelaySentinel</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
11
gpsrelaysentinel/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
12
gpsrelaysentinel/src/stage/AndroidManifest.xml
Normal 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>
|
||||||
|
|
||||||
6
gpsrelaysentinel/src/stage/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Put flavor specific strings here -->
|
||||||
|
|
||||||
|
</resources>
|
||||||
1
libgpsrelaysentinel/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
25
libgpsrelaysentinel/build.gradle
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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 21
|
||||||
|
targetSdkVersion 30
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
8
libgpsrelaysentinel/build.properties
Normal 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
libgpsrelaysentinel/proguard-rules.pro
vendored
Normal 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 *;
|
||||||
|
#}
|
||||||
27
libgpsrelaysentinel/src/main/AndroidManifest.xml
Normal 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.libgpsrelaysentinel">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service
|
||||||
|
android:name=".service.GpsSubscribeReceiverService"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="cc.winboll.studio.libgpsrelaysentinel.action.RECEIVE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:name=".receiver.GpsSubscribeObserverReceiver">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name=".receiver.GpsSubscribeObserverReceiver"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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.libgpsrelaysentinel.model.GpsSubscribeConst;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.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("data",result);
|
||||||
|
appContext.sendBroadcast(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAllSubscribe(){
|
||||||
|
subscribeMap.clear();
|
||||||
|
SubscribeLocationManager.getInstance().clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, GpsSubscribeMsg> getSubscribeMap() {
|
||||||
|
return subscribeMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/05/07 10:26
|
||||||
|
*/
|
||||||
|
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.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 SubscribeLocationManager(){
|
||||||
|
subscribeConfigMap = new HashMap<String, GpsSubscribeMsg>();
|
||||||
|
subscriberPointMap = new HashMap<String, LocationPoint>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SubscribeLocationManager getInstance(){
|
||||||
|
if(instance == null){
|
||||||
|
instance = new SubscribeLocationManager();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putSubscribeConfig(final String sid,final GpsSubscribeMsg msg){
|
||||||
|
subscribeConfigMap.put(sid,msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initSubscriberPoint(final String sid,double lat,double lng){
|
||||||
|
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSubscriberPoint(final String sid,double lat,double lng){
|
||||||
|
subscriberPointMap.put(sid,new LocationPoint(lat,lng,System.currentTimeMillis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocationPoint getLastPoint(final String sid){
|
||||||
|
return subscriberPointMap.get(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GpsSubscribeMsg getSubscribeConfig(final String sid){
|
||||||
|
return subscribeConfigMap.get(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNeedPush(final 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 result = 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)
|
||||||
|
));
|
||||||
|
result = result * GpsSubscribeConst.EARTH_RADIUS;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSubscribe(final String sid){
|
||||||
|
subscribeConfigMap.remove(sid);
|
||||||
|
subscriberPointMap.remove(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAll(){
|
||||||
|
subscribeConfigMap.clear();
|
||||||
|
subscriberPointMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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";
|
||||||
|
|
||||||
|
//超时毫秒
|
||||||
|
public static final long SUBSCRIBE_TIME_OUT = 5000;
|
||||||
|
|
||||||
|
//地球半径 距离计算常量
|
||||||
|
public static final double EARTH_RADIUS = 6378137.0;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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;
|
||||||
|
|
||||||
|
public GpsSubscribeResult(String subscribeUniqueId,
|
||||||
|
int resultCode,
|
||||||
|
String resultDesc,
|
||||||
|
int gpsRunningState,
|
||||||
|
long realEffectiveInterval,
|
||||||
|
long currentTimeStamp) {
|
||||||
|
this.subscribeUniqueId = subscribeUniqueId;
|
||||||
|
this.resultCode = resultCode;
|
||||||
|
this.resultDesc = resultDesc;
|
||||||
|
this.gpsRunningState = gpsRunningState;
|
||||||
|
this.realEffectiveInterval = realEffectiveInterval;
|
||||||
|
this.currentTimeStamp = currentTimeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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.libgpsrelaysentinel.model.GpsSubscribeConst;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.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("data");
|
||||||
|
if(listener != null && result != null){
|
||||||
|
listener.onResultBack(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnSubscribeResultListener{
|
||||||
|
void onResultBack(GpsSubscribeResult result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.service;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/05/07 10:46
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeResult;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.LocationPoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外消息接收服务:外部App可bind或start
|
||||||
|
* 收到GPS消息后,通过本地广播回调给外部App
|
||||||
|
*/
|
||||||
|
public final class GpsSubscribeReceiverService extends Service {
|
||||||
|
|
||||||
|
// 外部回调监听
|
||||||
|
public interface GpsMessageListener {
|
||||||
|
void onGpsLocation(LocationPoint point, GpsSubscribeMsg config);
|
||||||
|
void onSubscribeResult(GpsSubscribeResult result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<GpsMessageListener> listeners = new CopyOnWriteArrayList<GpsMessageListener>();
|
||||||
|
private final IBinder localBinder = new LocalBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 外部App绑定服务
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return localBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 外部App startService 入口:接收订阅请求
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (intent != null && intent.getParcelableExtra("req") != null) {
|
||||||
|
GpsSubscribeMsg msg = intent.getParcelableExtra("req");
|
||||||
|
handleSubscribeRequest(msg);
|
||||||
|
}
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理订阅请求:发送给管理器,并回执
|
||||||
|
private void handleSubscribeRequest(final GpsSubscribeMsg msg) {
|
||||||
|
// 加入订阅管理
|
||||||
|
cc.winboll.studio.libgpsrelaysentinel.manager.GpsSubscribeManager
|
||||||
|
.getInstance().addSubscribe(msg);
|
||||||
|
cc.winboll.studio.libgpsrelaysentinel.manager.SubscribeLocationManager
|
||||||
|
.getInstance().putSubscribeConfig(msg.getSubscribeUniqueId(), msg);
|
||||||
|
|
||||||
|
// 回执成功
|
||||||
|
GpsSubscribeResult result = new GpsSubscribeResult(
|
||||||
|
msg.getSubscribeUniqueId(),
|
||||||
|
GpsSubscribeConst.RESULT_SUCCESS,
|
||||||
|
"subscribe ok",
|
||||||
|
GpsSubscribeConst.GPS_STATE_LOCATED,
|
||||||
|
1000,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
sendSubscribeResultBroadcast(result);
|
||||||
|
notifySubscribeResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供内部(GPS服务)调用:推送定位消息
|
||||||
|
public void pushLocation(final LocationPoint point, final GpsSubscribeMsg config) {
|
||||||
|
sendLocationBroadcast(point, config);
|
||||||
|
notifyGpsLocation(point, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 广播回调(跨进程/外部App接收) ----------
|
||||||
|
private void sendLocationBroadcast(final LocationPoint point, final GpsSubscribeMsg config) {
|
||||||
|
Intent intent = new Intent(GpsSubscribeConst.ACTION_GPS_LOCATION);
|
||||||
|
intent.putExtra("point", point);
|
||||||
|
intent.putExtra("config", config);
|
||||||
|
sendBroadcast(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendSubscribeResultBroadcast(final GpsSubscribeResult result) {
|
||||||
|
Intent intent = new Intent(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK);
|
||||||
|
intent.putExtra("data", result);
|
||||||
|
sendBroadcast(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 本地Binder(同进程直接回调) ----------
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
public GpsSubscribeReceiverService getService() {
|
||||||
|
return GpsSubscribeReceiverService.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(final GpsMessageListener l) {
|
||||||
|
if (l != null && !listeners.contains(l)) {
|
||||||
|
listeners.add(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(final GpsMessageListener l) {
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyGpsLocation(final LocationPoint point, final GpsSubscribeMsg config) {
|
||||||
|
for (GpsMessageListener l : listeners) {
|
||||||
|
l.onGpsLocation(point, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySubscribeResult(final GpsSubscribeResult result) {
|
||||||
|
for (GpsMessageListener l : listeners) {
|
||||||
|
l.onSubscribeResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
listeners.clear();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package cc.winboll.studio.libgpsrelaysentinel.view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/05/07 10:27
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
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.libgpsrelaysentinel.R;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeConst;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeMsg;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.model.GpsSubscribeResult;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.receiver.GpsSubscribeObserverReceiver;
|
||||||
|
import cc.winboll.studio.libgpsrelaysentinel.util.TimeCountUtil;
|
||||||
|
|
||||||
|
public final class GpsSubscribeControlView extends LinearLayout {
|
||||||
|
|
||||||
|
private RadioGroup rgSubMode;
|
||||||
|
private RadioButton rbAll;
|
||||||
|
private RadioButton rbStep;
|
||||||
|
private LinearLayout layoutStepSetting;
|
||||||
|
private EditText etStepMeter;
|
||||||
|
|
||||||
|
private Switch mSwitchSubscribe;
|
||||||
|
private TextView mTvCountTip;
|
||||||
|
|
||||||
|
private TimeCountUtil mTimeCountUtil;
|
||||||
|
private GpsSubscribeObserverReceiver mResultReceiver;
|
||||||
|
private String currentSubscribeSid;
|
||||||
|
private boolean isSubscribeSuccess;
|
||||||
|
|
||||||
|
public GpsSubscribeControlView(Context context) {
|
||||||
|
super(context);
|
||||||
|
initView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GpsSubscribeControlView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
initView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initView(){
|
||||||
|
setOrientation(VERTICAL);
|
||||||
|
inflate(getContext(),R.layout.view_gps_subscribe_control,this);
|
||||||
|
|
||||||
|
rgSubMode = findViewById(R.id.rg_sub_mode);
|
||||||
|
rbAll = findViewById(R.id.rb_all);
|
||||||
|
rbStep = findViewById(R.id.rb_step);
|
||||||
|
layoutStepSetting = findViewById(R.id.layout_step_setting);
|
||||||
|
etStepMeter = findViewById(R.id.et_step_meter);
|
||||||
|
|
||||||
|
mSwitchSubscribe = findViewById(R.id.switch_subscribe);
|
||||||
|
mTvCountTip = findViewById(R.id.tv_count_tip);
|
||||||
|
|
||||||
|
initModeSwitch();
|
||||||
|
initCountUtil();
|
||||||
|
initReceiver();
|
||||||
|
initSwitchEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initModeSwitch(){
|
||||||
|
rgSubMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||||
|
layoutStepSetting.setVisibility(checkedId == R.id.rb_step ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCountUtil(){
|
||||||
|
mTimeCountUtil = new TimeCountUtil(new TimeCountUtil.OnCountListener() {
|
||||||
|
@Override
|
||||||
|
public void onTimeOut() {
|
||||||
|
if(!isSubscribeSuccess){
|
||||||
|
mSwitchSubscribe.setChecked(false);
|
||||||
|
mTvCountTip.setText("订阅超时,已自动关闭");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initReceiver(){
|
||||||
|
mResultReceiver = new GpsSubscribeObserverReceiver();
|
||||||
|
mResultReceiver.setOnSubscribeResultListener(new GpsSubscribeObserverReceiver.OnSubscribeResultListener() {
|
||||||
|
@Override
|
||||||
|
public void onResultBack(GpsSubscribeResult result) {
|
||||||
|
if(currentSubscribeSid.equals(result.getSubscribeUniqueId())){
|
||||||
|
isSubscribeSuccess = true;
|
||||||
|
mTimeCountUtil.cancel();
|
||||||
|
mTvCountTip.setText("订阅已生效,通讯正常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(GpsSubscribeConst.ACTION_SUBSCRIBE_CALLBACK);
|
||||||
|
getContext().registerReceiver(mResultReceiver,filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSwitchEvent(){
|
||||||
|
mSwitchSubscribe.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
if(isChecked){
|
||||||
|
startSubscribe();
|
||||||
|
}else{
|
||||||
|
stopSubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startSubscribe(){
|
||||||
|
isSubscribeSuccess = false;
|
||||||
|
currentSubscribeSid = UUID.randomUUID().toString();
|
||||||
|
mTvCountTip.setText("等待订阅返回中...");
|
||||||
|
|
||||||
|
int subMode = GpsSubscribeConst.SUB_TYPE_ALL;
|
||||||
|
float stepMeter = 10.0f;
|
||||||
|
|
||||||
|
if(rbStep.isChecked()){
|
||||||
|
subMode = GpsSubscribeConst.SUB_TYPE_STEP_DISTANCE;
|
||||||
|
try{
|
||||||
|
stepMeter = Float.parseFloat(etStepMeter.getText().toString().trim());
|
||||||
|
}catch (Exception e){
|
||||||
|
stepMeter = 10.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GpsSubscribeMsg msg = new GpsSubscribeMsg(
|
||||||
|
getContext().getPackageName(),
|
||||||
|
subMode,
|
||||||
|
stepMeter,
|
||||||
|
GpsSubscribeConst.SUBSCRIBE_TYPE_LOCATION,
|
||||||
|
1000,
|
||||||
|
1.0f,
|
||||||
|
true,
|
||||||
|
currentSubscribeSid
|
||||||
|
);
|
||||||
|
|
||||||
|
Intent intent = new Intent(GpsSubscribeConst.ACTION_SUBSCRIBE_REQUEST);
|
||||||
|
intent.putExtra("req",msg);
|
||||||
|
getContext().sendBroadcast(intent);
|
||||||
|
|
||||||
|
mTimeCountUtil.start(GpsSubscribeConst.SUBSCRIBE_TIME_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopSubscribe(){
|
||||||
|
mTimeCountUtil.cancel();
|
||||||
|
isSubscribeSuccess = false;
|
||||||
|
mTvCountTip.setText("订阅已关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release(){
|
||||||
|
mTimeCountUtil.cancel();
|
||||||
|
if(mResultReceiver != null){
|
||||||
|
getContext().unregisterReceiver(mResultReceiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BIN
libgpsrelaysentinel/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
libgpsrelaysentinel/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
libgpsrelaysentinel/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
libgpsrelaysentinel/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,99 @@
|
|||||||
|
<?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="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="订阅模式:"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/rg_sub_mode"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rb_all"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="全消息订阅"
|
||||||
|
android:checked="true"/>
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rb_step"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="步长订阅"/>
|
||||||
|
</RadioGroup>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_step_setting"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="移动步长阈值(米):"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_step_meter"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:text="10"
|
||||||
|
android:gravity="center"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="GPS订阅总开关"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_subscribe"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_count_tip"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="未订阅"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#666666"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
5
libgpsrelaysentinel/src/main/res/values-v21/styles.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="AppTheme" parent="@android:style/Theme.Material.Light">
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
8
libgpsrelaysentinel/src/main/res/values/strings.xml
Normal 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>
|
||||||
5
libgpsrelaysentinel/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="AppTheme" parent="@android:style/Theme.Holo.Light">
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -77,3 +77,8 @@
|
|||||||
// RegExpUtils 项目编译设置
|
// RegExpUtils 项目编译设置
|
||||||
//include ':regexputils'
|
//include ':regexputils'
|
||||||
//rootProject.name = "regexputils"
|
//rootProject.name = "regexputils"
|
||||||
|
|
||||||
|
// GPSRelaySentinel 项目编译设置
|
||||||
|
//include ':gpsrelaysentinel'
|
||||||
|
//include ':libgpsrelaysentinel'
|
||||||
|
//rootProject.name = "gpsrelaysentinel"
|
||||||
|
|||||||