Compare commits

...

11 Commits

Author SHA1 Message Date
e99d7ebc06 <positions>APK 15.12.21 release Publish. 2026-05-04 11:31:30 +08:00
07a286e7e0 重构主界面菜单与调试模式UI控制逻辑
- 将空转切换菜单项(item_idle_switch)从toolbar_main.xml移至新建的toolbar_main_idle.xml
- 调试模式(App.isDebugging()为真)时加载toolbar_main_idle.xml菜单,非调试模式不加载
- onCreate时根据调试状态控制日志区域(tv_idle_log)的显示/隐藏
- onResume时通过invalidateOptionsMenu()重新加载菜单,确保状态同步
2026-05-04 11:28:10 +08:00
7761c80275 更新基础类库 2026-05-04 10:57:41 +08:00
e1f6c3de05 <positions>APK 15.12.20 release Publish. 2026-05-04 09:52:15 +08:00
84c616cbda feat: 实现应用空转状态持久化与MainService动态数据源切换
1. App.java:
   - 添加SharedPreferences持久化空转状态,App重启后自动恢复
   - setAppIdleRunning()中增加MainService服务重启逻辑,同步空转状态

2. MainService.java:
   - onCreate/onStartCommand: 检测空转状态,自动启动IdleGpsService
   - 重构GPS监听逻辑,新增mIdleGpsListener实例与中央处理方法
   - startGpsLocation/stopGpsLocation: 根据空转状态自动切换系统GPS与IdleGpsService数据源
   - 前台通知栏显示 [IDLE RUNNING] 标识及全精度(%.15f)经纬度数据
   - 通知内容区分"空转GPS"与真实"GPS位置"前缀

3. IdleGpsService.java:
   - 添加服务启动时的Toast提示反馈
   - 修复MainService中IdleGpsService类型不匹配问题

4. MainActivity.java:
   - 简化空转切换逻辑,移除冗余的IdleGpsService手动启停代码
2026-05-04 09:41:42 +08:00
9d868f216c feat(IdleGpsService): 实现圆形轨迹GPS模拟并优化服务启动逻辑
- 重构IdleGpsService,将固定坐标模拟改为圆形轨迹动态生成
- 添加角动量(bearing)递增机制,实现坐标沿圆周持续运动
- 新增calculatePosition()方法,基于锚点坐标和半径计算轨迹位置
- 在MainActivity.onResume中根据空转状态自动启动GPS服务
- 更新build.properties版本号(buildCount 21->23)
2026-05-04 03:41:57 +08:00
79b0320fb9 feat(IdleGpsService): 添加手动控制空转GPS服务的启动/停止逻辑
- 新增 IdleGpsService.start() 和 stop() 方法,支持外部手动控制服务状态
- 在 MainActivity 菜单切换空转状态时,联动启动/停止空转GPS服务
- 优化模拟坐标更新间隔从1秒调整为5秒,降低资源消耗
- 添加空转日志记录,便于追踪GPS服务状态变更
2026-05-03 18:29:32 +08:00
173290333d feat(GPS): 添加空转GPS模拟服务与动态源切换逻辑
1. 新增IdleGpsService,在空转模式下定时发送模拟GPS坐标(默认北京)
2. LocationActivity支持动态切换GPS源:空转时使用IdleGpsService,正常时使用MainService
3. 优化onResume逻辑,确保空转状态变更时GPS监听源实时同步
4. 统一反注册逻辑,避免多服务监听冲突
2026-05-03 16:49:59 +08:00
f9ef5ab16e refactor(LocationActivity): 优化代码结构与新增空转状态提示
1. 新增Toolbar副标题动态显示应用空转运行状态
2. 重构类结构,生命周期方法移至底部,工具方法置顶
3. 规范日志输出格式,统一使用"invoke"等标识
4. 增强变量不可变性声明,多处添加final修饰符
5. 简化冗余逻辑判断与条件检查
6. 清理冗余注释,更新Javadoc描述
2026-05-03 16:02:21 +08:00
748661b984 源码整理 2026-05-03 15:30:13 +08:00
2cfc29845e fix(MainActivity): 优化定位权限校验逻辑与空转状态UI刷新
1. 服务开关开启前增加定位权限前置校验,无权限时阻止开启并提示
2. 空转关闭时自动检测权限,无权限则强制停止GPS服务
3. 权限被拒绝时强制关闭服务开关并禁用管理按钮
4. 新增Toolbar副标题显示空转运行状态提示
5. 新增管理按钮状态刷新,空转中强制可点击并追加提示文字
6. 移除冗余注释,精简接口定义
2026-05-03 15:22:18 +08:00
9 changed files with 645 additions and 337 deletions

View File

@@ -81,8 +81,8 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.15.2'
api 'cc.winboll.studio:libappbase:15.15.11'
api 'cc.winboll.studio:libaes:15.15.9'
api 'cc.winboll.studio:libappbase:15.15.21'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun May 03 05:39:16 GMT 2026
stageCount=20
#Mon May 04 11:31:30 HKT 2026
stageCount=22
libraryProject=
baseVersion=15.12
publishVersion=15.12.19
buildCount=7
baseBetaVersion=15.12.20
publishVersion=15.12.21
buildCount=0
baseBetaVersion=15.12.22

View File

@@ -5,6 +5,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
@@ -25,6 +26,7 @@ import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.services.MainService;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -59,9 +61,13 @@ public class App extends GlobalApplication {
//===================== 全局静态常量与变量 =====================
public static final String TAG = "App";
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
private static App sInstance;
private static final String SP_NAME = "app_idle_config";
private static final String SP_KEY_IDLE_RUNNING = "is_idle_running";
// 应用全局空转状态标记
public static boolean isAppIdleRunning = false;
private static boolean isAppIdleRunning = false;
//===================== 空转状态对外方法 =====================
/**
@@ -83,23 +89,67 @@ public class App extends GlobalApplication {
LogUtils.d(TAG, "setAppIdleRunning -> 传入参数 idleRunning = " + idleRunning);
if (isDebugging()) {
isAppIdleRunning = idleRunning;
LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效");
saveIdleRunningToSp(idleRunning);
LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效并持久化");
// 重启MainService服务以同步新的空转状态stopService对未运行服务无效故无需判断状态
if (sInstance != null) {
LogUtils.i(TAG, "setAppIdleRunning -> 重启MainService服务以同步空转状态");
Intent serviceIntent = new Intent(sInstance, MainService.class);
sInstance.stopService(serviceIntent);
sInstance.startService(serviceIntent);
}
} else {
LogUtils.i(TAG, "setAppIdleRunning -> 非调试模式,空转设置无效");
LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless.");
}
}
/**
* 从SharedPreferences加载空转状态
*/
private static void loadIdleRunningFromSp() {
try {
if (sInstance != null) {
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
isAppIdleRunning = sp.getBoolean(SP_KEY_IDLE_RUNNING, false);
LogUtils.i(TAG, "loadIdleRunningFromSp -> 从SP加载空转状态" + isAppIdleRunning);
}
} catch (Exception e) {
LogUtils.e(TAG, "loadIdleRunningFromSp -> 加载空转状态失败:" + e.getMessage());
isAppIdleRunning = false;
}
}
/**
* 保存空转状态到SharedPreferences
* @param idleRunning 空转状态
*/
private static void saveIdleRunningToSp(boolean idleRunning) {
try {
if (sInstance != null) {
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putBoolean(SP_KEY_IDLE_RUNNING, idleRunning).apply();
LogUtils.i(TAG, "saveIdleRunningToSp -> 空转状态已保存:" + idleRunning);
}
} catch (Exception e) {
LogUtils.e(TAG, "saveIdleRunningToSp -> 保存空转状态失败:" + e.getMessage());
}
}
//===================== 应用生命周期 =====================
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
LogUtils.i(TAG, "onCreate -> 全局Application初始化开始");
setIsDebugging(BuildConfig.DEBUG);
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
loadIdleRunningFromSp();
LogUtils.i(TAG, "onCreate -> 全局组件初始化全部完成");
}

View File

@@ -35,7 +35,16 @@ import cc.winboll.studio.positions.activities.WinBoLLActivity;
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
import cc.winboll.studio.positions.utils.ServiceUtil;
import cc.winboll.studio.positions.services.IdleGpsService;
import cc.winboll.studio.positions.services.MainService;
import cc.winboll.studio.positions.R;
import android.os.Handler;
import android.os.Looper;
import android.os.Handler;
import android.os.Handler;
import android.os.Looper;
import android.content.Intent;
import android.os.Handler;
/**
* 主页面控制器
@@ -46,7 +55,7 @@ import cc.winboll.studio.positions.R;
* 4. 全局权限申请与权限结果回调处理
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026/05/03 12:23:00
* @EditTime 2026/05/03 15:26:13
* @EditTime 2026/05/03 15:42:15
*/
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
@@ -71,16 +80,7 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
* 应用空转状态回调内部接口
*/
public interface OnAppIdleRunningListener {
/**
* 接收空转开关状态变更
* @param isRunning 空转是否开启
*/
void onIdleStatusChange(boolean isRunning);
/**
* 接收空转日志消息
* @param log 日志文本
*/
void onIdleLogReceive(String log);
}
@@ -112,17 +112,31 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
mADsBannerView = findViewById(R.id.adsbanner);
initAppIdleHandler();
// 初始化自动根据空转状态刷新日志面板
refreshIdleLogLayout();
// 根据调试模式控制日志区域的显示
if (App.isDebugging()) {
mScrollIdleLog.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "onCreate -> 调试模式,显示日志区域");
} else {
mScrollIdleLog.setVisibility(View.GONE);
LogUtils.d(TAG, "onCreate -> 非调试模式,隐藏日志区域");
}
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume -> 页面恢复可见");
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().start();
}
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
}
// 重新加载菜单以根据当前调试状态刷新
invalidateOptionsMenu();
LogUtils.d(TAG, "onResume -> 重新加载菜单完成");
}
@Override
@@ -136,9 +150,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
}
// ===================== 初始化相关方法 =====================
/**
* 初始化顶部导航栏
*/
private void initToolbar() {
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
@@ -148,9 +159,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
LogUtils.d(TAG, "initToolbar -> 顶部工具栏初始化完毕");
}
/**
* 初始化全部UI控件与绑定事件
*/
private void initViews() {
mTvIdleLog = (TextView) findViewById(R.id.tv_idle_log);
mScrollIdleLog = (ScrollView) findViewById(R.id.scroll_idle_log);
@@ -164,31 +172,31 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//切换开关时先校验权限,无权限直接强制关闭开关
if(isChecked && !checkLocationPermissions()){
mServiceSwitch.setChecked(false);
Toast.makeText(MainActivity.this,"未获取定位权限无法开启GPS服务",Toast.LENGTH_SHORT).show();
return;
}
LogUtils.d(TAG, "onCheckedChanged -> 服务开关状态变更isChecked = " + isChecked);
if (isChecked && !checkLocationPermissions()) {
requestLocationPermissions();
return;
}
if (isChecked) {
ServiceUtil.startAutoService(MainActivity.this);
} else {
ServiceUtil.stopAutoService(MainActivity.this);
}
mManagePositionsButton.setEnabled(isChecked);
refreshManageButtonState();
}
});
LogUtils.i(TAG, "initViews -> 全部UI控件初始化绑定完成");
}
/**
* 初始化空转处理器并绑定监听回调
*/
private void initAppIdleHandler() {
AppIdleRunningModeHandler.init(MainActivity.this);
setOnAppIdleRunningListener(new OnAppIdleRunningListener() {
@Override
public void onIdleStatusChange(boolean isRunning) {
// 状态变更立刻刷新边框与日志
refreshIdleLogLayout();
appendIdleLog("IdleRunning Status : " + isRunning);
}
@@ -201,9 +209,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
LogUtils.i(TAG, "initAppIdleHandler -> 空转处理器初始化与监听绑定完成");
}
/**
* 设置主布局主题背景色
*/
private void setLLMainBackgroundColor() {
TypedArray typedArray = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
int colorAccent = typedArray.getColor(0, Color.GRAY);
@@ -213,10 +218,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
}
// ===================== 空转日志输出工具 =====================
/**
* 追加空转日志并自动滚动至底部
* @param logText 需要追加的日志文本
*/
private void appendIdleLog(final String logText) {
runOnUiThread(new Runnable() {
@Override
@@ -233,11 +234,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
});
}
/**
* 日志面板样式自动控制
* 非空转 = 清空日志 + 去除边框
* 空转开启 = 恢复边框
*/
private void refreshIdleLogLayout() {
runOnUiThread(new Runnable() {
@Override
@@ -245,19 +241,52 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
if (App.isAppIdleRunning()) {
mScrollIdleLog.setBackgroundResource(R.drawable.shape_log_border);
} else {
//关闭空转时检测权限无权限强制关闭GPS开关
if(!checkLocationPermissions()){
mServiceSwitch.setChecked(false);
ServiceUtil.stopAutoService(MainActivity.this);
}
mTvIdleLog.setText("");
mScrollIdleLog.setBackgroundColor(Color.TRANSPARENT);
LogUtils.d(TAG, "refreshIdleLogLayout -> 非空转状态:日志清空、边框已移除");
}
refreshToolbarSubTitle();
refreshManageButtonState();
}
});
}
private void refreshToolbarSubTitle() {
if(getSupportActionBar() == null){
return;
}
if(App.isAppIdleRunning()){
getSupportActionBar().setSubtitle("当前处于空转运行状态");
}else{
getSupportActionBar().setSubtitle("");
}
}
/**
* 空转状态专属:按钮强制可点击 + 文字追加提示
*/
private void refreshManageButtonState() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (App.isAppIdleRunning()) {
mManagePositionsButton.setText("位置与任务管理【当前空转中】");
mManagePositionsButton.setEnabled(true);
} else {
mManagePositionsButton.setText("位置与任务管理");
boolean serviceEnable = AppConfigsUtil.getInstance(MainActivity.this).isEnableMainService(true);
mManagePositionsButton.setEnabled(serviceEnable);
}
}
});
}
// ===================== 权限处理相关 =====================
/**
* 检查前台+后台定位权限
* @return 权限是否全部通过
*/
private boolean checkLocationPermissions() {
int foregroundPerm = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
boolean hasForegroundPerm = (foregroundPerm == PackageManager.PERMISSION_GRANTED);
@@ -270,9 +299,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
return hasForegroundPerm && hasBackgroundPerm;
}
/**
* 发起定位权限动态申请
*/
private void requestLocationPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
@@ -301,6 +327,10 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
requestLocationPermissions();
} else {
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
//权限被拒绝,强制关闭开关
mServiceSwitch.setChecked(false);
ServiceUtil.stopAutoService(MainActivity.this);
mManagePositionsButton.setEnabled(false);
}
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -317,6 +347,7 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
AESThemeUtil.inflateMenu(this, menu);
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_main_idle, menu);
}
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return true;
@@ -332,14 +363,12 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
} else if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, "onOptionsItemSelected -> 进入开发工具菜单");
} else if (menuItemId == R.id.item_idle_switch) {
// 菜单切换应用空转状态
boolean idleNow = App.isAppIdleRunning();
boolean idleNew = !idleNow;
App.setAppIdleRunning(idleNew);
AppIdleRunningModeHandler.sendIdleSwitch(idleNew);
AppIdleRunningModeHandler.sendIdleLog("菜单手动切换空转状态:" + idleNew);
LogUtils.d(TAG, "onOptionsItemSelected -> 空转状态已切换,当前:" + idleNew);
// 切换立刻刷新UI样式
refreshIdleLogLayout();
} else if (menuItemId == R.id.item_settings) {
Intent intent = new Intent();
@@ -355,9 +384,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
return true;
}
/**
* 跳转位置管理页面
*/
public void onPositions(View view) {
LogUtils.d(TAG, "onPositions -> 跳转位置任务管理页面");
startActivity(new Intent(MainActivity.this, LocationActivity.class));

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.positions.activities;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 18:22
* @Describe 位置列表页面适配MainService GPS接口+规范服务交互+完善生命周期)
*/
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@@ -16,18 +11,23 @@ import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.MainActivity;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.adapters.PositionAdapter;
import cc.winboll.studio.positions.models.PositionModel;
import cc.winboll.studio.positions.services.IdleGpsService;
import cc.winboll.studio.positions.services.MainService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -35,59 +35,50 @@ import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Java 7 语法适配:
* 1. 服务绑定用匿名内部类实现 ServiceConnection
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
* 3. 所有位置/任务操作通过 MainService 接口执行
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025/09/29 18:22:00
* @EditTime 2026/05/03 16:36:54
* @Describe 位置列表页面适配MainService GPS接口、规范服务交互、完善生命周期资源释放新增应用空转状态副标题提示
*/
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
// 常量
public static final String TAG = "LocationActivity";
private Toolbar mToolbar;
// 成员属性有序排版
private Toolbar mToolbar;
private RecyclerView mRvPosition;
private PositionAdapter mPositionAdapter;
// MainService 引用+绑定状态AtomicBoolean 确保多线程状态可见性)
private MainService mMainService;
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
// 标记 Adapter 是否已初始化(避免重复初始化/销毁后初始化)
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
// ---------------------- 新增GPS监听核心变量 ----------------------
private MainService.GpsUpdateListener mGpsUpdateListener; // GPS监听实例
private PositionModel mCurrentGpsPos; // 缓存当前GPS位置供页面使用
// 本地位置缓存解决服务数据未同步时Adapter空数据问题
private MainService.GpsUpdateListener mGpsUpdateListener;
private PositionModel mCurrentGpsPos;
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
// 服务连接Java 7 匿名内部类实现,强化状态同步+数据预加载)
private ServiceConnection mServiceConnection = new ServiceConnection() {
// 服务连接回调
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 1. 安全获取服务实例(避免强转失败+服务未就绪)
public void onServiceConnected(final ComponentName name, final IBinder service) {
LogUtils.d(TAG, "onServiceConnected invoke");
if (!(service instanceof MainService.LocalBinder)) {
LogUtils.e(TAG, "服务绑定失败Binder类型不匹配非MainService.LocalBinder");
LogUtils.e(TAG, "服务绑定失败Binder类型不匹配");
isServiceBound.set(false);
return;
}
try {
MainService.LocalBinder binder = (MainService.LocalBinder) service;
final MainService.LocalBinder binder = (MainService.LocalBinder) service;
mMainService = binder.getService();
// 2. 标记服务绑定成功(原子操作,确保多线程可见)
isServiceBound.set(true);
LogUtils.d(TAG, "MainService绑定成功,开始同步数据+初始化Adapter");
LogUtils.d(TAG, "MainService bind success");
// 3. 同步服务数据到本地缓存核心先同步数据再初始化Adapter
syncDataFromMainService();
// 4. 注册GPS监听确保监听在Adapter前初始化数据不丢失
registerGpsListener();
// 5. 初始化Adapter传入本地缓存+服务实例,数据非空)
initPositionAdapter();
} catch (Exception e) {
LogUtils.d(TAG, "服务绑定后初始化失败:" + e.getMessage());
} catch (final Exception e) {
LogUtils.e(TAG, "服务绑定异常:" + e.getMessage());
isServiceBound.set(false);
mMainService = null;
showToast("服务初始化失败,无法加载数据");
@@ -95,16 +86,15 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
}
@Override
public void onServiceDisconnected(ComponentName name) {
LogUtils.w(TAG, "MainService断开连接,清空引用+标记状态");
// 1. 清空服务引用+标记绑定状态
public void onServiceDisconnected(final ComponentName name) {
LogUtils.w(TAG, "MainService 服务断开");
mMainService = null;
isServiceBound.set(false);
// 2. 标记Adapter未初始化下次绑定需重新初始化
isAdapterInited.set(false);
}
};
// 接口方法实现
@Override
public Activity getActivity() {
return this;
@@ -115,187 +105,143 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_location);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
startActivity(new Intent(LocationActivity.this, MainActivity.class));
finish();
}
});
// 1. 初始化视图优先执行避免Adapter初始化时视图为空
initView();
// 2. 初始化GPS监听提前创建避免绑定服务后空指针
initGpsUpdateListener();
// 3. 绑定MainService最后执行确保视图/监听已就绪)
bindMainService();
/**
* 刷新空转状态副标题
*/
private void refreshIdleStatusTitle() {
LogUtils.d(TAG, "refreshIdleStatusTitle invoke");
if (mToolbar == null) {
LogUtils.w(TAG, "Toolbar为空跳过刷新");
return;
}
final boolean idleStatus = App.isAppIdleRunning();
if (idleStatus) {
mToolbar.setSubtitle("当前状态:应用正在空转运行");
LogUtils.d(TAG, "检测应用空转状态为 : " + idleStatus);
} else {
mToolbar.setSubtitle(getTag());
}
}
/**
* 初始化视图RecyclerView- 确保视图先于Adapter初始化
* 初始化页面控件
*/
private void initView() {
mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
// 1. 显式设置布局管理器避免Adapter设置时无布局管理器崩溃
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
LogUtils.d(TAG, "initView invoke");
mRvPosition = findViewById(R.id.rv_position_list);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRvPosition.setLayoutManager(layoutManager);
// 2. 初始化本地缓存(避免首次加载时缓存为空)
mLocalPosCache.clear();
LogUtils.d(TAG, "视图初始化完成(布局管理器+本地缓存已就绪)");
}
/**
* 绑定MainServiceJava 7 显式Intent强化绑定安全性
* 绑定后台服务
*/
private void bindMainService() {
// 1. 避免重复绑定快速重建Activity时防止多绑定
LogUtils.d(TAG, "bindMainService invoke");
if (isServiceBound.get()) {
LogUtils.w(TAG, "无需重复绑定MainService已绑定");
LogUtils.w(TAG, "服务已绑定,无需重复执行");
return;
}
Intent serviceIntent = new Intent(this, MainService.class);
// 2. 绑定服务BIND_AUTO_CREATE服务不存在时自动创建增加绑定成功率
boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
final Intent serviceIntent = new Intent(this, MainService.class);
final boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
if (!bindSuccess) {
LogUtils.e(TAG, "发起MainService绑定请求失败服务未找到/系统限制)");
LogUtils.e(TAG, "发起服务绑定请求失败");
showToast("服务绑定失败,无法加载位置数据");
} else {
LogUtils.d(TAG, "MainService绑定请求已发起");
}
}
/**
* 从MainService同步数据本地缓存核心解决Adapter空数据问题
* 作用1. 服务数据优先同步到本地Adapter基于本地缓存初始化
* 2. 避免服务数据更新时直接操作Adapter通过缓存中转
* 同步服务数据本地缓存
*/
private void syncDataFromMainService() {
// 1. 安全校验(服务未绑定/服务空,用本地缓存兜底)
LogUtils.d(TAG, "syncDataFromMainService invoke");
if (!isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "同步数据:服务未就绪,使用本地缓存(当前缓存量=" + mLocalPosCache.size() + "");
LogUtils.w(TAG, "服务未就绪,使用本地缓存");
return;
}
try {
// 2. 从服务获取最新位置数据(同步操作,确保数据拿到后再返回)
ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
// 3. 同步到本地缓存(清空旧数据+添加新数据,避免重复)
synchronized (mLocalPosCache) { // 加锁避免多线程操作缓存冲突
final ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
synchronized (mLocalPosCache) {
mLocalPosCache.clear();
if (servicePosList != null && !servicePosList.isEmpty()) {
mLocalPosCache.addAll(servicePosList);
}
}
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
+ ",本地缓存数=" + mLocalPosCache.size());
} catch (Exception e) {
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
// 异常时保留本地缓存避免Adapter无数据
LogUtils.w(TAG, "同步失败,使用本地缓存兜底(缓存量=" + mLocalPosCache.size() + "");
LogUtils.d(TAG, "同步完成,缓存数量 : " + mLocalPosCache.size());
} catch (final Exception e) {
LogUtils.e(TAG, "数据同步异常 : " + e.getMessage());
}
}
/**
* 初始化PositionAdapter核心优化基于本地缓存初始化避免空数据
* 初始化列表适配器
*/
private void initPositionAdapter() {
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
LogUtils.w(TAG, "Adapter初始化跳过"
+ "已初始化=" + isAdapterInited.get()
+ ",服务绑定=" + isServiceBound.get()
+ ",视图就绪=" + (mRvPosition != null));
LogUtils.d(TAG, "initPositionAdapter invoke");
if (isAdapterInited.get() || !isServiceBound.get()
|| mMainService == null || mRvPosition == null) {
LogUtils.w(TAG, "适配器初始化条件不满足,跳过");
return;
}
try {
// 2. 基于本地缓存初始化Adapter缓存已同步服务数据非空
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
@Override
public void onDeleteClick(final int position) {
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示", "是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener(){
LogUtils.d(TAG, "onDeleteClick position = " + position);
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示",
"是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onNo() {
@Override
public void onNo() {
}
@Override
public void onYes() {
// 安全校验(索引有效+服务绑定+缓存非空)
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + "");
return;
}
PositionModel deletePos = mLocalPosCache.get(position);
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
// 步骤1调用服务删除确保服务数据一致性
mMainService.removePosition(deletePos.getPositionId());
// 步骤2删除本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.remove(position);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemRemoved(position);
showToast("删除位置成功:" + deletePos.getMemo());
LogUtils.d(TAG, "删除位置完成ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
}
}
});
}
@Override
public void onYes() {
if (position < 0 || position >= mLocalPosCache.size()) {
LogUtils.w(TAG, "删除索引参数非法");
return;
}
final PositionModel deletePos = mLocalPosCache.get(position);
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
mMainService.removePosition(deletePos.getPositionId());
synchronized (mLocalPosCache) {
mLocalPosCache.remove(position);
}
mPositionAdapter.notifyItemRemoved(position);
showToast("删除位置成功:" + deletePos.getMemo());
}
}
});
}
});
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
@Override
public void onSavePositionClick(int position, PositionModel updatedPos) {
// 安全校验(索引有效+服务绑定+数据非空)
if (!isServiceBound.get() || mMainService == null
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
public void onSavePositionClick(final int position, final PositionModel updatedPos) {
LogUtils.d(TAG, "onSavePositionClick position = " + position);
if (position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
LogUtils.w(TAG, "保存参数非法");
showToast("服务未就绪,保存失败");
return;
}
// 步骤1调用服务更新确保服务数据一致性
mMainService.updatePosition(updatedPos);
// 步骤2更新本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.set(position, updatedPos);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemChanged(position);
showToast("保存位置成功:" + updatedPos.getMemo());
LogUtils.d(TAG, "保存位置完成ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
}
});
// 5. 设置Adapter到RecyclerView最后一步确保Adapter已配置完成
mRvPosition.setAdapter(mPositionAdapter);
// 6. 标记Adapter已初始化避免重复初始化
isAdapterInited.set(true);
LogUtils.d(TAG, "PositionAdapter初始化完成基于本地缓存数据量=" + mLocalPosCache.size() + "");
} catch (Exception e) {
LogUtils.d(TAG, "Adapter初始化失败" + e.getMessage());
LogUtils.d(TAG, "适配器初始化完成");
} catch (final Exception e) {
LogUtils.e(TAG, "适配器初始化异常 : " + e.getMessage());
isAdapterInited.set(false);
mPositionAdapter = null;
showToast("位置列表初始化失败,请重试");
@@ -303,95 +249,91 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
}
/**
* 显示ToastJava 7 显式Toast.makeText避免空Context
* 通用Toast弹窗
*/
private void showToast(String content) {
if (isFinishing() || isDestroyed()) { // 避免Activity销毁后弹Toast崩溃
LogUtils.w(TAG, "Activity已销毁,跳过Toast" + content);
private void showToast(final String content) {
if (isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "页面已销毁,取消Toast");
return;
}
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
}
// ---------------------- 页面交互新增位置逻辑保留适配GPS数据 ----------------------
/**
* 新增位置调用服务addPosition()可选用当前GPS位置初始化新位置
* 新增位置按钮事件
*/
public void addNewPosition(View view) {
// 1. 隐藏软键盘(避免软键盘遮挡操作)
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
public void addNewPosition(final View view) {
LogUtils.d(TAG, "addNewPosition invoke");
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null && getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
// 2. 安全校验(服务未绑定,不允许新增)
if (!isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "新增位置失败MainService未绑定");
LogUtils.w(TAG, "服务未绑定,无法新增");
showToast("服务未就绪,无法新增位置");
return;
}
// 3. 创建新位置模型优化优先用当前GPS位置初始化无则用默认值
PositionModel newPos = new PositionModel();
newPos.setPositionId(PositionModel.genPositionId()); // 生成唯一ID需PositionModel实现
final PositionModel newPos = new PositionModel();
newPos.setPositionId(PositionModel.genPositionId());
if (mCurrentGpsPos != null) {
newPos.setLongitude(mCurrentGpsPos.getLongitude());
newPos.setLatitude(mCurrentGpsPos.getLatitude());
newPos.setMemo("当前GPS位置可编辑");
} else {
newPos.setLongitude(116.404267); // 北京经度(默认值)
newPos.setLatitude(39.915119); // 北京纬度(默认值)
newPos.setLongitude(116.404267);
newPos.setLatitude(39.915119);
newPos.setMemo("默认位置(可编辑备注)");
}
newPos.setIsSimpleView(true); // 默认简单视图
newPos.setIsEnableRealPositionDistance(true); // 启用距离计算依赖GPS
newPos.setIsSimpleView(true);
newPos.setIsEnableRealPositionDistance(true);
// 4. 调用服务新增+同步本地缓存(确保缓存与服务一致)
mMainService.addPosition(newPos);
synchronized (mLocalPosCache) {
mLocalPosCache.add(newPos);
}
LogUtils.d(TAG, "通过服务新增位置ID=" + newPos.getPositionId() + ",纬度=" + newPos.getLatitude() + "(缓存已同步)");
LogUtils.d(TAG, "新增位置成功ID : " + newPos.getPositionId());
// 5. 刷新Adapter基于缓存操作确保数据立即显示
if (isAdapterInited.get() && mPositionAdapter != null) {
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
}
showToast("新增位置成功已启用GPS距离计算");
}
// ---------------------- 新增GPS监听初始化+注册/反注册(核心适配逻辑) ----------------------
/**
* 初始化GPS监听实现MainService.GpsUpdateListener接收实时GPS数据
* 初始化GPS监听实例
*/
private void initGpsUpdateListener() {
LogUtils.d(TAG, "initGpsUpdateListener()");
LogUtils.d(TAG, "initGpsUpdateListener invoke");
mGpsUpdateListener = new MainService.GpsUpdateListener() {
@Override
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
public void onGpsPositionUpdated(final PositionModel currentGpsPos) {
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "GPS位置更新数据为空或Activity已销毁");
return;
}
// 缓存当前GPS位置供页面其他逻辑使用
mCurrentGpsPos = currentGpsPos;
LogUtils.d(TAG, String.format("收到GPS更新纬度=%.4f,经度=%.4f"
, currentGpsPos.getLatitude(), currentGpsPos.getLongitude()));
// 安全更新UI避免Activity销毁后操作视图崩溃
((TextView)findViewById(R.id.tv_latitude)).setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
((TextView)findViewById(R.id.tv_longitude)).setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
// 设置格式化后的时间字符串
long currentTime = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
String timeStr = sdf.format(new Date(currentTime));
// 设置当前时间
((TextView)findViewById(R.id.tv_timenow)).setText("现在时间:" + timeStr);
LogUtils.d(TAG, "GPS更新 -> 纬度:" + currentGpsPos.getLatitude() + " 经度:" + currentGpsPos.getLongitude());
final TextView tvLat = (TextView) findViewById(R.id.tv_latitude);
final TextView tvLon = (TextView) findViewById(R.id.tv_longitude);
final TextView tvTime = (TextView) findViewById(R.id.tv_timenow);
tvLat.setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
tvLon.setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
final long currentTime = System.currentTimeMillis();
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
final String timeStr = sdf.format(new Date(currentTime));
tvTime.setText("现在时间:" + timeStr);
}
@Override
public void onGpsStatusChanged(String status) {
if (status == null || isFinishing() || isDestroyed()) return;
LogUtils.d(TAG, "GPS状态变化" + status);
public void onGpsStatusChanged(final String status) {
if (status == null || isFinishing() || isDestroyed()) {
return;
}
LogUtils.d(TAG, "GPS状态变更 : " + status);
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
ToastUtils.show("GPS提示" + status);
}
@@ -399,111 +341,147 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
};
}
/**
* 注册GPS监听调用MainService的PUBLIC方法绑定监听
/**
* 刷新GPS监听源(根据空转状态自动切换)
*/
private void refreshGpsListener() {
LogUtils.d(TAG, "refreshGpsListener invoke");
// 统一注销旧监听(防止重复注册)
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
if (mMainService != null) {
try { mMainService.unregisterGpsUpdateListener(mGpsUpdateListener); } catch (Exception e) {}
}
// 重新注册正确的监听
registerGpsListener();
}
/**
* 注册GPS监听
*/
private void registerGpsListener() {
// 安全校验避免Activity销毁/服务未绑定/监听为空时注册)
if (isFinishing() || isDestroyed() || !isServiceBound.get() || mMainService == null || mGpsUpdateListener == null) {
LogUtils.w(TAG, "GPS监听注册跳过Activity状态异常/依赖未就绪");
LogUtils.d(TAG, "registerGpsListener invoke");
if (isFinishing() || isDestroyed() || mGpsUpdateListener == null) {
return;
}
try {
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "GPS监听已注册");
} catch (Exception e) {
LogUtils.d(TAG, "GPS监听注册失败" + e.getMessage());
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().registerGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "空转GPS监听注册成功");
} else {
if (mMainService != null) {
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "系统GPS监听注册成功");
}
}
} catch (final Exception e) {
LogUtils.e(TAG, "GPS注册异常 : " + e.getMessage());
}
}
/**
* 反注册GPS监听调用MainService的PUBLIC方法解绑监听核心防内存泄漏+数据异常)
* 反注册GPS监听
*/
private void unregisterGpsListener() {
// 避免Activity销毁后调用服务方法防止空指针/服务已解绑)
if (mMainService == null || mGpsUpdateListener == null) {
LogUtils.w(TAG, "GPS监听反注册跳过服务/监听未初始化");
LogUtils.d(TAG, "unregisterGpsListener invoke");
if (mGpsUpdateListener == null) {
return;
}
try {
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "GPS监听已反注册");
// 尝试从空转服务注销
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
} catch (Exception e) {
LogUtils.d(TAG, "GPS监听反注册失败" + e.getMessage());
// ignore
}
try {
// 尝试从主服务注销
if (mMainService != null) {
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
}
} catch (final Exception e) {
LogUtils.e(TAG, "GPS反注册异常 : " + e.getMessage());
}
}
/**
* 页面可见时同步数据(解决快速切回时数据未更新问题)
* 场景快速关闭再打开Activity服务已绑定但数据未重新同步
*/
// 生命周期方法
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate invoke");
setContentView(R.layout.activity_location);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
LogUtils.d(TAG, "点击返回导航按钮");
final Intent intent = new Intent(LocationActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
});
refreshIdleStatusTitle();
initView();
initGpsUpdateListener();
bindMainService();
}
@Override
protected void onResume() {
super.onResume();
// 1. 服务已绑定但Adapter未初始化重新同步数据+初始化Adapter
LogUtils.d(TAG, "onResume invoke");
refreshIdleStatusTitle();
refreshGpsListener(); // 根据当前空转状态刷新GPS源
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
LogUtils.d(TAG, "onResume服务已绑定但Adapter未初始化重新同步数据");
syncDataFromMainService();
initPositionAdapter();
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
} else if (isServiceBound.get() && mMainService != null && mPositionAdapter != null) {
syncDataFromMainService();
mPositionAdapter.notifyDataSetChanged();
LogUtils.d(TAG, "onResume刷新位置数据与服务同步");
}
}
/**
* 页面不可见时暂停操作(避免后台操作导致数据异常)
*/
@Override
protected void onPause() {
super.onPause();
// 避免后台时仍执行UI刷新如GPS更新触发的视图操作
LogUtils.d(TAG, "onPause页面不可见暂停UI相关操作");
LogUtils.d(TAG, "onPause invoke");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy开始释放资源");
LogUtils.d(TAG, "onDestroy invoke开始释放资源");
// 1. 反注册GPS监听优先执行避免服务持有Activity引用导致内存泄漏
unregisterGpsListener();
// 2. 释放Adapter资源反注册可能的监听避免内存泄漏
if (mPositionAdapter != null) {
mPositionAdapter.release();
mPositionAdapter = null; // 清空引用帮助GC回收
LogUtils.d(TAG, "Adapter资源释放");
mPositionAdapter = null;
LogUtils.d(TAG, "Adapter资源释放完成");
}
// 3. 解绑MainService最后执行确保其他资源已释放
if (isServiceBound.get()) {
try {
unbindService(mServiceConnection);
LogUtils.d(TAG, "MainService解绑完成");
} catch (IllegalArgumentException e) {
// 捕获“服务未绑定”异常(快速开关时可能出现,避免崩溃)
LogUtils.d(TAG, "解绑MainService失败服务未绑定可能已提前解绑");
LogUtils.d(TAG, "服务解绑完成");
} catch (final IllegalArgumentException e) {
LogUtils.e(TAG, "解绑异常:服务已提前解绑");
}
// 重置绑定状态+服务引用
isServiceBound.set(false);
mMainService = null;
}
// 4. 清空本地缓存+GPS引用帮助GC回收
synchronized (mLocalPosCache) {
mLocalPosCache.clear();
}
mCurrentGpsPos = null;
mGpsUpdateListener = null;
isAdapterInited.set(false);
LogUtils.d(TAG, "所有资源释放完onDestroy执行结束");
LogUtils.d(TAG, "全部资源释放完");
}
// ---------------------- 移除重复定义LocalBinder 统一在 MainService 中定义 ----------------------
// 说明原LocationActivity中的LocalBinder是重复定义MainService已实现会导致类型强转失败
// 此处删除该类确保Activity绑定服务时强转的是MainService中的LocalBinder
}

View File

@@ -0,0 +1,182 @@
package cc.winboll.studio.positions.services;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
import cc.winboll.studio.positions.models.PositionModel;
import java.util.ArrayList;
import java.util.List;
/**
* 空转GPS模拟服务
* 在应用空转模式下模拟系统GPS服务向客户端发送坐标数据
*/
public class IdleGpsService {
private static final long MOCK_INTERVAL_MS = 5000; // 模拟坐标更新间隔5秒
private static final long BEARING_INTERVAL_MS = 1000; // 角动量递增间隔1秒
private static final double EARTH_RADIUS_M = 6371000; // 地球平均半径(米)
private static final double ANCHOR_LAT = 39.9042; // 固定锚点纬度(北京)
private static final double ANCHOR_LON = 116.4074; // 固定锚点经度(北京)
private static final double CIRCLE_RADIUS_M = 50; // 移动轨迹半径(米)
private static IdleGpsService instance;
private final List<MainService.GpsUpdateListener> listeners = new ArrayList<>();
private final Handler handler;
private final Runnable updateRunnable;
private final Runnable bearingRunnable;
private final PositionModel mockPosition;
private boolean isRunning;
private int currentBearing = 1; // 当前角动量度数1-360
private IdleGpsService() {
handler = new Handler(Looper.getMainLooper());
mockPosition = new PositionModel();
mockPosition.setPositionId("mock_idle_pos");
mockPosition.setMemo("空转模拟坐标");
updateRunnable = new Runnable() {
@Override
public void run() {
if (isRunning) {
calculatePosition();
notifyListeners(mockPosition);
AppIdleRunningModeHandler.sendIdleLog("模拟GPS数据更新 -> 纬度:" + mockPosition.getLatitude() + ", 经度:" + mockPosition.getLongitude() + ", 角度:" + currentBearing + "°");
handler.postDelayed(this, MOCK_INTERVAL_MS);
}
}
};
bearingRunnable = new Runnable() {
@Override
public void run() {
if (isRunning) {
currentBearing++;
if (currentBearing > 360) {
currentBearing = 1;
}
handler.postDelayed(this, BEARING_INTERVAL_MS);
}
}
};
}
private void calculatePosition() {
double bearingRad = Math.toRadians(currentBearing);
double angularDistance = CIRCLE_RADIUS_M / EARTH_RADIUS_M;
double anchorLatRad = Math.toRadians(ANCHOR_LAT);
double anchorLonRad = Math.toRadians(ANCHOR_LON);
double newLatRad = Math.asin(
Math.sin(anchorLatRad) * Math.cos(angularDistance) +
Math.cos(anchorLatRad) * Math.sin(angularDistance) * Math.cos(bearingRad)
);
double newLonRad = anchorLonRad + Math.atan2(
Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(anchorLatRad),
Math.cos(angularDistance) - Math.sin(anchorLatRad) * Math.sin(newLatRad)
);
mockPosition.setLatitude(Math.toDegrees(newLatRad));
mockPosition.setLongitude(Math.toDegrees(newLonRad));
}
public static IdleGpsService getInstance() {
if (instance == null) {
instance = new IdleGpsService();
}
return instance;
}
/**
* 启动空转模拟服务
*/
public void start() {
if (isRunning) return;
isRunning = true;
currentBearing = 1;
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已启动");
notifyStatusChange("空转GPS服务已启动");
ToastUtils.show("空转GPS服务已启动");
handler.post(updateRunnable);
handler.post(bearingRunnable);
}
/**
* 停止空转模拟服务
*/
public void stop() {
if (!isRunning) return;
isRunning = false;
handler.removeCallbacks(updateRunnable);
handler.removeCallbacks(bearingRunnable);
notifyStatusChange("空转GPS服务已停止");
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已停止");
}
/**
* 注册GPS监听
*/
public void registerGpsUpdateListener(MainService.GpsUpdateListener listener) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
if (!isRunning) {
start();
}
}
/**
* 注销GPS监听
*/
public void unregisterGpsUpdateListener(MainService.GpsUpdateListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
if (listeners.isEmpty() && isRunning) {
stopMockUpdate();
}
}
private void startMockUpdate() {
if (isRunning) return;
isRunning = true;
currentBearing = 1;
notifyStatusChange("空转GPS服务已启动");
handler.post(updateRunnable);
handler.post(bearingRunnable);
}
private void stopMockUpdate() {
isRunning = false;
handler.removeCallbacks(updateRunnable);
handler.removeCallbacks(bearingRunnable);
notifyStatusChange("空转GPS服务已停止");
}
private void notifyListeners(PositionModel pos) {
synchronized (listeners) {
for (MainService.GpsUpdateListener listener : listeners) {
listener.onGpsPositionUpdated(pos);
}
}
}
private void notifyStatusChange(String status) {
synchronized (listeners) {
for (MainService.GpsUpdateListener listener : listeners) {
listener.onGpsStatusChanged(status);
}
}
}
public boolean isRunning() {
return isRunning;
}
}

View File

@@ -22,6 +22,7 @@ import android.os.Looper;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.models.PositionModel;
import cc.winboll.studio.positions.models.PositionTaskModel;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
@@ -54,6 +55,35 @@ public class MainService extends Service {
void onGpsStatusChanged(String status);
}
// 空转GPS监听实例用于注册到IdleGpsService
private final GpsUpdateListener mIdleGpsListener = new GpsUpdateListener() {
@Override
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
handleGpsPositionUpdate(currentGpsPos);
}
@Override
public void onGpsStatusChanged(String status) {
handleGpsStatusChange(status);
}
};
// 中央处理GPS 位置更新
private void handleGpsPositionUpdate(PositionModel pos) {
if (pos == null) return;
syncCurrentGpsPosition(pos);
DistanceCalculatorUtil.getInstance(MainService.this).checkAllTaskTriggerCondition(pos);
String src = App.isAppIdleRunning() ? " (空转)" : "";
LogUtils.d(TAG, "GPS位置更新纬度=" + pos.getLatitude() + ",经度=" + pos.getLongitude() + src);
}
// 中央处理GPS 状态变化
private void handleGpsStatusChange(String status) {
LogUtils.d(TAG, "GPS状态变化" + status);
notifyAllGpsStatusListeners(status);
updateNotificationGpsStatus(status);
}
// 任务更新监听接口Java 7 风格供Adapter监听任务变化
public interface TaskUpdateListener {
void onTaskUpdated();
@@ -77,6 +107,7 @@ public class MainService extends Service {
private final ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>(); // 位置数据列表
private final ArrayList<PositionTaskModel> mAllTasks = new ArrayList<PositionTaskModel>();// 任务数据列表
private static PositionModel _mCurrentGpsPosition; // 当前GPS定位数据
private boolean isListeningToIdleGps = false; // 当前是否监听空转GPS
// 服务相关变量Java 7 显式声明,保持原逻辑)
MyServiceConnection mMyServiceConnection;
@@ -366,6 +397,9 @@ public class MainService extends Service {
}
if (mAppConfigsUtil.isEnableMainService(true)) {
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().start();
}
run(); // 启动服务核心逻辑
}
}
@@ -382,6 +416,9 @@ public class MainService extends Service {
// 启动前台服务Java 7 显式调用,无方法引用)
String initialStatus = "[ Positions ] is in Service.";
if (App.isAppIdleRunning()) {
initialStatus += " [IDLE RUNNING]";
}
NotificationUtil.createForegroundServiceNotification(this, initialStatus);
startForeground(NotificationUtil.FOREGROUND_SERVICE_NOTIFICATION_ID,
NotificationUtil.createForegroundServiceNotification(this, initialStatus));
@@ -599,21 +636,28 @@ public class MainService extends Service {
if (!_mIsServiceRunning || _mCurrentGpsPosition == null) {
return;
}
// 格式化通知内容Java 7 String.format无String.join等Java 8+方法
final String gpsStatus = String.format(
"GPS位置北纬%.4f° 东经%.4f° | 可见位置:%d个",
// 根据空转状态决定通知前缀区分空转GPS与真实GPS
String prefix = App.isAppIdleRunning() ? "空转GPS" : "GPS位置";
// 格式化通知内容Java 7 String.format使用%.15f显示全部GPS精度
String gpsStatus = String.format(
"%s北纬%.15f° 东经%.15f° | 可见位置:%d个",
prefix,
_mCurrentGpsPosition.getLatitude(),
_mCurrentGpsPosition.getLongitude(),
mVisiblePositionIds.size()
);
if (App.isAppIdleRunning()) {
gpsStatus += " [IDLE RUNNING]";
}
final String finalGpsStatus = gpsStatus;
// 主线程判断+切换Java 7 匿名内部类)
if (Looper.myLooper() == Looper.getMainLooper()) {
NotificationUtil.updateForegroundServiceStatus(this, gpsStatus);
NotificationUtil.updateForegroundServiceStatus(this, finalGpsStatus);
} else {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
NotificationUtil.updateForegroundServiceStatus(MainService.this, gpsStatus);
NotificationUtil.updateForegroundServiceStatus(MainService.this, finalGpsStatus);
}
});
}
@@ -634,6 +678,9 @@ public class MainService extends Service {
if (intent != null) {
isSettingToEnable = intent.getBooleanExtra(EXTRA_IS_SETTING_TO_ENABLE, false);
if (isSettingToEnable) {
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().start();
}
run(); // 重启服务核心逻辑(保证服务启动后进入运行状态)
}
}
@@ -701,10 +748,8 @@ public class MainService extends Service {
gpsPos.setPositionId("CURRENT_GPS_POS");
gpsPos.setMemo("实时GPS位置");
// 同步GPS位置+刷新距离+日志(原逻辑保留)
syncCurrentGpsPosition(gpsPos);
DistanceCalculatorUtil.getInstance(MainService.this).checkAllTaskTriggerCondition(gpsPos);
LogUtils.d(TAG, "GPS位置更新纬度=" + location.getLatitude() + ",经度=" + location.getLongitude());
// 调用中央处理方法
handleGpsPositionUpdate(gpsPos);
}
}
@@ -725,9 +770,7 @@ public class MainService extends Service {
statusDesc = "GPS状态临时不可用遮挡";
break;
}
LogUtils.d(TAG, statusDesc);
notifyAllGpsStatusListeners(statusDesc);
updateNotificationGpsStatus(statusDesc);
handleGpsStatusChange(statusDesc);
}
}
@@ -736,10 +779,7 @@ public class MainService extends Service {
// GPS启用时更新状态+通知+重启定位Java 7 基础逻辑)
if (provider.equals(LocationManager.GPS_PROVIDER)) {
isGpsEnabled = true;
String statusDesc = "GPS已开启用户手动打开";
LogUtils.d(TAG, statusDesc);
notifyAllGpsStatusListeners(statusDesc);
updateNotificationGpsStatus("GPS已开启正在获取位置...");
handleGpsStatusChange("GPS已开启用户手动打开");
startGpsLocation();
}
}
@@ -750,10 +790,7 @@ public class MainService extends Service {
if (provider.equals(LocationManager.GPS_PROVIDER)) {
isGpsEnabled = false;
_mCurrentGpsPosition = null;
String statusDesc = "GPS已关闭用户手动关闭";
LogUtils.w(TAG, statusDesc);
notifyAllGpsStatusListeners(statusDesc);
updateNotificationGpsStatus("GPS已关闭请在设置中开启");
handleGpsStatusChange("GPS已关闭用户手动关闭");
ToastUtils.show("GPS已关闭无法获取位置请在设置中开启");
}
}
@@ -801,8 +838,27 @@ public class MainService extends Service {
/**
* 启动GPS定位Java 7 异常处理无try-with-resources显式捕获SecurityException
* 【关键修改】根据应用空转状态切换数据源IdleGpsService 或 系统GPS
*/
private void startGpsLocation() {
// 检查空转状态:如果处于空转,使用 IdleGpsService
if (App.isAppIdleRunning()) {
if (isListeningToIdleGps) return; // 已在监听空转GPS无需重复注册
stopGpsLocation(); // 停止系统GPS监听如果正在运行
IdleGpsService.getInstance().registerGpsUpdateListener(mIdleGpsListener);
isListeningToIdleGps = true;
LogUtils.d(TAG, "启动GPS定位使用空转模拟数据");
handleGpsStatusChange("空转GPS监听中...");
return;
}
// 系统GPS逻辑
if (isListeningToIdleGps) {
// 之前是空转现在切换到系统GPS
IdleGpsService.getInstance().unregisterGpsUpdateListener(mIdleGpsListener);
isListeningToIdleGps = false;
}
if (!checkGpsReady()) {
return;
}
@@ -824,12 +880,11 @@ public class MainService extends Service {
lastGpsPos.setLatitude(lastKnownLocation.getLatitude());
lastGpsPos.setLongitude(lastKnownLocation.getLongitude());
lastGpsPos.setPositionId("CURRENT_GPS_POS");
syncCurrentGpsPosition(lastGpsPos);
handleGpsPositionUpdate(lastGpsPos);
LogUtils.d(TAG, "已获取缓存GPS位置纬度=" + lastKnownLocation.getLatitude());
} else {
String tip = "无缓存GPS位置等待实时定位...";
LogUtils.d(TAG, tip);
notifyAllGpsStatusListeners(tip);
handleGpsStatusChange(tip);
updateNotificationGpsStatus("GPS搜索中请移至开阔地带");
}
@@ -837,33 +892,40 @@ public class MainService extends Service {
// 定位权限异常Java 7 显式捕获无Lambda异常处理
String error = "启动GPS失败权限异常" + e.getMessage();
LogUtils.e(TAG, error);
notifyAllGpsStatusListeners(error);
handleGpsStatusChange(error);
isGpsPermissionGranted = false;
updateNotificationGpsStatus("定位权限异常无法获取GPS");
} catch (Exception e) {
// 其他异常如LocationManager为空、系统服务异常等
String error = "启动GPS失败" + e.getMessage();
LogUtils.e(TAG, error);
notifyAllGpsStatusListeners(error);
handleGpsStatusChange(error);
updateNotificationGpsStatus("GPS启动失败尝试重试...");
}
}
/**
* 停止GPS定位Java 7 异常处理,移除监听器避免内存泄漏)
* 【关键修改】根据当前监听源停止对应的服务
*/
private void stopGpsLocation() {
// 校验参数:避免空指针+权限未授予时调用
if (mLocationManager != null && mGpsLocationListener != null && isGpsPermissionGranted) {
try {
mLocationManager.removeUpdates(mGpsLocationListener);
String tip = "GPS定位已停止移除监听器";
LogUtils.d(TAG, tip);
notifyAllGpsStatusListeners(tip);
} catch (Exception e) {
String error = "停止GPS失败" + e.getMessage();
LogUtils.e(TAG, error);
notifyAllGpsStatusListeners(error);
if (isListeningToIdleGps) {
IdleGpsService.getInstance().unregisterGpsUpdateListener(mIdleGpsListener);
isListeningToIdleGps = false;
LogUtils.d(TAG, "停止GPS定位已注销空转GPS监听");
} else {
// 校验参数:避免空指针+权限未授予时调用
if (mLocationManager != null && mGpsLocationListener != null && isGpsPermissionGranted) {
try {
mLocationManager.removeUpdates(mGpsLocationListener);
String tip = "GPS定位已停止移除监听器";
LogUtils.d(TAG, tip);
handleGpsStatusChange(tip);
} catch (Exception e) {
String error = "停止GPS失败" + e.getMessage();
LogUtils.e(TAG, error);
handleGpsStatusChange(error);
}
}
}
}
@@ -915,14 +977,19 @@ public class MainService extends Service {
*/
void updateNotificationGpsStatus(final String statusText) {
if (_mIsServiceRunning) {
String text = statusText;
if (App.isAppIdleRunning()) {
text += " [IDLE RUNNING]";
}
final String finalText = text;
// 判断当前线程是否为主线程避免UI操作在子线程
if (Looper.myLooper() == Looper.getMainLooper()) {
NotificationUtil.updateForegroundServiceStatus(this, statusText);
NotificationUtil.updateForegroundServiceStatus(this, finalText);
} else {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
NotificationUtil.updateForegroundServiceStatus(MainService.this, statusText);
NotificationUtil.updateForegroundServiceStatus(MainService.this, finalText);
}
});
}

View File

@@ -2,10 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_idle_switch"
android:title="空转状态切换"/>
<item
android:id="@+id/item_settings"
android:title="Settings"/>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_idle_switch"
android:title="空转状态切换"/>
</menu>