后台服务版初版
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue Sep 30 18:06:22 GMT 2025
|
||||
#Tue Sep 30 21:17:41 GMT 2025
|
||||
stageCount=4
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.3
|
||||
buildCount=10
|
||||
buildCount=19
|
||||
baseBetaVersion=15.0.4
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- 在后台使用位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false"/>
|
||||
|
||||
@@ -1,32 +1,186 @@
|
||||
package cc.winboll.studio.positions;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogActivity;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||
import cc.winboll.studio.positions.services.DistanceRefreshService;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||
|
||||
/**
|
||||
* 主页面:仅负责
|
||||
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
||||
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
||||
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
public static final String TAG = "MainActivity";
|
||||
|
||||
// UI 控件:服务控制开关、顶部工具栏
|
||||
private SwitchCompat mServiceSwitch;
|
||||
private Toolbar mToolbar;
|
||||
// 服务相关:服务实例、绑定状态标记
|
||||
private DistanceRefreshService mDistanceService;
|
||||
private boolean isServiceBound = false;
|
||||
|
||||
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
||||
private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||
/**
|
||||
* 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
||||
*/
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
|
||||
DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||
mDistanceService = binder.getService();
|
||||
isServiceBound = true;
|
||||
// 绑定后立即同步开关状态,避免UI与服务实际状态不一致
|
||||
syncSwitchState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务意外断开(如服务崩溃):重置服务实例和绑定状态
|
||||
*/
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mDistanceService = null;
|
||||
isServiceBound = false;
|
||||
// 断开后同步开关状态(从SP读取上次保存的状态)
|
||||
syncSwitchState();
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------- Activity 生命周期(核心:初始化UI、绑定服务、释放资源) ----------------------
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setContentView(R.layout.activity_main); // 关联主页面布局
|
||||
|
||||
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
||||
initToolbar();
|
||||
// 2. 初始化服务控制开关(核心功能:绑定开关点击事件、读取SP状态)
|
||||
initServiceSwitch();
|
||||
// 3. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
||||
bindDistanceService();
|
||||
}
|
||||
|
||||
public void onPositions(View view) {
|
||||
startActivity(new Intent(this, LocationActivity.class));
|
||||
}
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
|
||||
if (isServiceBound) {
|
||||
unbindService(mServiceConn);
|
||||
isServiceBound = false;
|
||||
mDistanceService = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onLog(View view) {
|
||||
LogActivity.startLogActivity(this);
|
||||
}
|
||||
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
||||
/**
|
||||
* 初始化顶部 Toolbar,设置页面标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
||||
setSupportActionBar(mToolbar);
|
||||
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle("位置管理");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务控制开关:读取SP状态、绑定点击事件
|
||||
*/
|
||||
private void initServiceSwitch() {
|
||||
mServiceSwitch = (SwitchCompat) findViewById(R.id.switch_service_control); // 显式强转
|
||||
|
||||
// 2. 绑定开关状态变化监听(Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener)
|
||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
AppConfigsModel.saveBean(MainActivity.this, new AppConfigsModel(true));
|
||||
// 开关打开:启动服务(通过startService确保服务独立运行,不受Activity绑定影响)
|
||||
startService(new Intent(MainActivity.this, DistanceRefreshService.class));
|
||||
|
||||
} else {
|
||||
AppConfigsModel.saveBean(MainActivity.this, new AppConfigsModel(false));
|
||||
// 开关关闭:先解绑服务(避免服务被Activity持有),再停止服务
|
||||
if (isServiceBound) {
|
||||
unbindService(mServiceConn);
|
||||
isServiceBound = false;
|
||||
}
|
||||
stopService(new Intent(MainActivity.this, DistanceRefreshService.class));
|
||||
}
|
||||
// 状态变化后同步开关UI(确保UI与服务实际状态一致)
|
||||
syncSwitchState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------- 核心功能2:服务状态同步与绑定 ----------------------
|
||||
/**
|
||||
* 同步服务开关状态:优先以服务实时状态为准,无服务则读SP
|
||||
*/
|
||||
private void syncSwitchState() {
|
||||
if (mServiceSwitch == null) {
|
||||
return; // 开关未初始化,直接返回
|
||||
}
|
||||
|
||||
if (isServiceBound && mDistanceService != null) {
|
||||
ToastUtils.show("位置服务已启动");
|
||||
} else {
|
||||
ToastUtils.show("位置服务已关闭");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定服务(仅用于获取服务状态,不启动服务)
|
||||
*/
|
||||
private void bindDistanceService() {
|
||||
Intent serviceIntent = new Intent(this, DistanceRefreshService.class);
|
||||
// 绑定服务:BIND_AUTO_CREATE 表示若服务未启动则创建(仅为获取状态,后续由开关控制启停)
|
||||
bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
// ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
|
||||
/**
|
||||
* 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
|
||||
* 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
|
||||
*/
|
||||
public void onPositions(View view) {
|
||||
// 从配置文件读取服务状态(避免依赖服务绑定,提升稳定性)
|
||||
AppConfigsModel bean = AppConfigsModel.loadBean(MainActivity.this, AppConfigsModel.class);
|
||||
boolean isServiceRunning = (bean == null) ? false : bean.isEnableDistanceRefreshService();
|
||||
|
||||
if (!isServiceRunning) {
|
||||
ToastUtils.show("请先启动位置服务,否则无法加载数据");
|
||||
return; // 服务未启动,不跳转
|
||||
}
|
||||
|
||||
// 服务已启动:跳转到位置管理页
|
||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至“日志页(LogActivity)”(按钮点击触发,需在布局中设置 android:onClick="onLog")
|
||||
* 无服务状态限制,直接跳转
|
||||
*/
|
||||
public void onLog(View view) {
|
||||
LogActivity.startLogActivity(this); // 调用LogActivity静态方法跳转(保留原逻辑)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,443 +3,251 @@ package cc.winboll.studio.positions.activities;
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:22
|
||||
* @Describe 当前位置实时显示 + 位置列表实时距离计算
|
||||
* @Describe 位置列表页面(Java 7 兼容,完全依赖DistanceRefreshService数据)
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.os.IBinder;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
import com.google.android.gms.tasks.OnSuccessListener;
|
||||
import cc.winboll.studio.positions.services.DistanceRefreshService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
|
||||
/**
|
||||
* 实时定位活动窗口:
|
||||
* 1. 申请定位必需权限(精确定位)
|
||||
* 2. 初始化FusedLocationProviderClient(谷歌官方定位服务,兼容所有安卓版本)
|
||||
* 3. 实时监听位置变化,更新显示经度、纬度 + 同步给Adapter计算实时距离
|
||||
* 4. 右下角圆形悬浮按钮(含大写P字母,支持添加位置)
|
||||
* 5. 位置列表支持实时距离显示(按isEnableRealPositionDistance控制)
|
||||
* 核心逻辑:
|
||||
* 1. 启动前检查服务状态,未运行则拦截并提示
|
||||
* 2. 绑定服务后通过接口获取数据,不本地存储数据
|
||||
* 3. Adapter初始化仅传上下文+服务实例,数据从服务实时获取
|
||||
* 4. 严格Java 7语法:显式类型转换、匿名内部类、无Lambda
|
||||
*/
|
||||
public class LocationActivity extends AppCompatActivity {
|
||||
|
||||
public static final String TAG = "LocationActivity";
|
||||
|
||||
// SP配置常量(与服务保持一致,用于判断服务状态)
|
||||
private static final String SP_SERVICE_CONFIG = "service_config";
|
||||
private static final String KEY_SERVICE_RUNNING = "is_service_running";
|
||||
// 页面核心控件与变量
|
||||
private RecyclerView mRecyclerView;
|
||||
private PositionAdapter mAdapter;
|
||||
private DistanceRefreshService mDistanceService; // 服务实例(已实现DistanceServiceInterface)
|
||||
private boolean isServiceBound = false; // 服务绑定状态标记
|
||||
|
||||
// 1. 核心组件与常量定义(兼容Java 7,移除不必要final)
|
||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1004; // 定位权限请求码
|
||||
private FusedLocationProviderClient fusedLocationClient; // 定位核心客户端
|
||||
private LocationCallback locationCallback; // 位置变化监听器
|
||||
private LocationRequest locationRequest; // 定位请求配置(频率、精度等)
|
||||
private Location currentLocation; // 存储当前最新位置(用于同步给Adapter)
|
||||
// ---------------------- 服务连接(Java 7 匿名内部类实现) ----------------------
|
||||
private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// 显式类型转换(Java 7 不支持自动推断,必须强转)
|
||||
DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||
mDistanceService = binder.getService();
|
||||
isServiceBound = true;
|
||||
LogUtils.d("LocationActivity", "服务绑定成功,开始初始化Adapter");
|
||||
// 绑定成功后初始化Adapter(仅传上下文+服务实例)
|
||||
initAdapter();
|
||||
}
|
||||
|
||||
// UI控件(Java 7显式声明+强制转换)
|
||||
private TextView tvLongitude; // 经度显示
|
||||
private TextView tvLatitude; // 纬度显示
|
||||
private Button fabPButton; // 右下角圆形悬浮按钮(P字母)
|
||||
private RecyclerView rvPositionList; // 位置列表(RecyclerView)
|
||||
private PositionAdapter positionAdapter; // 列表Adapter(含实时距离逻辑)
|
||||
ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>(); // 位置数据集合
|
||||
ArrayList<PositionTaskModel> mPositionTasksList = new ArrayList<PositionTaskModel>(); // 位置数据集合
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
// 服务意外断开:置空引用+更新状态,避免空指针
|
||||
mDistanceService = null;
|
||||
isServiceBound = false;
|
||||
LogUtils.w("LocationActivity", "服务意外断开连接(可能被系统回收)");
|
||||
Toast.makeText(LocationActivity.this, "服务已断开,请重新进入页面", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------- 页面生命周期(严格管理服务绑定/资源) ----------------------
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_location);
|
||||
|
||||
// 绑定UI控件(Java 7显式强制转换)
|
||||
tvLongitude = (TextView) findViewById(R.id.tv_longitude);
|
||||
tvLatitude = (TextView) findViewById(R.id.tv_latitude);
|
||||
fabPButton = (Button) findViewById(R.id.fab_p_button);
|
||||
rvPositionList = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||
// 1. 优先检查服务状态:未运行则提示并关闭页面
|
||||
checkServiceRunningStatus();
|
||||
// 2. 初始化RecyclerView(基础配置+性能优化)
|
||||
initRecyclerViewConfig();
|
||||
// 3. 绑定服务(获取数据的唯一入口,自动创建服务)
|
||||
bindDistanceService();
|
||||
}
|
||||
|
||||
// 初始化核心逻辑:定位配置→悬浮按钮→列表Adapter→加载历史数据
|
||||
initLocationConfig();
|
||||
initFabPButton();
|
||||
initRecyclerViewAndAdapter();
|
||||
loadHistoryPositions();
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 1. 解绑服务(仅绑定状态下执行,避免异常)
|
||||
if (isServiceBound) {
|
||||
unbindService(mServiceConn);
|
||||
isServiceBound = false;
|
||||
mDistanceService = null; // 置空引用,帮助GC回收
|
||||
LogUtils.d("LocationActivity", "服务已解绑,避免内存泄漏");
|
||||
}
|
||||
// 2. 清理Adapter资源(调用Adapter内部销毁方法)
|
||||
if (mAdapter != null) {
|
||||
mAdapter.release();
|
||||
mAdapter = null;
|
||||
}
|
||||
// 3. 清理RecyclerView引用
|
||||
mRecyclerView = null;
|
||||
}
|
||||
|
||||
// 检查并申请定位权限(权限通过后启动实时定位)
|
||||
if (checkLocationPermissions()) {
|
||||
startRealTimeLocation();
|
||||
} else {
|
||||
requestLocationPermissions();
|
||||
// ---------------------- 核心初始化方法(服务状态检查+RecyclerView+Adapter) ----------------------
|
||||
/**
|
||||
* 检查服务运行状态(从SP读取,与服务状态保持一致)
|
||||
*/
|
||||
private void checkServiceRunningStatus() {
|
||||
// Java 7 显式获取SP实例(不使用方法链简化)
|
||||
SharedPreferences sp = getSharedPreferences(SP_SERVICE_CONFIG, Context.MODE_PRIVATE);
|
||||
// 读取服务状态:默认未运行(false)
|
||||
boolean isServiceRunning = sp.getBoolean(KEY_SERVICE_RUNNING, false);
|
||||
|
||||
if (!isServiceRunning) {
|
||||
// 服务未运行:提示用户并关闭页面
|
||||
Toast.makeText(this, "请先启动位置服务,否则无法加载数据", Toast.LENGTH_SHORT).show();
|
||||
finish(); // 关闭当前页面,返回上一级
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化定位配置(兼容Java 7,用LocationRequest.create()替代Builder)
|
||||
* 初始化RecyclerView(布局管理器+性能优化)
|
||||
*/
|
||||
private void initLocationConfig() {
|
||||
// 初始化定位客户端
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
// 定位请求配置(高精度、1秒更新一次,适配旧版Google Play Services)
|
||||
locationRequest = LocationRequest.create();
|
||||
locationRequest.setInterval(1000); // 定位更新间隔(1秒)
|
||||
locationRequest.setFastestInterval(500); // 最快更新间隔(500毫秒)
|
||||
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); // 优先GPS高精度定位
|
||||
|
||||
// 位置变化监听器(实时更新UI + 同步位置给Adapter计算距离)
|
||||
locationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(@NonNull LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
currentLocation = locationResult.getLastLocation(); // 更新当前最新位置
|
||||
if (currentLocation != null) {
|
||||
// 1. 更新页面经度、纬度显示
|
||||
double longitude = currentLocation.getLongitude();
|
||||
double latitude = currentLocation.getLatitude();
|
||||
tvLongitude.setText(String.format("当前经度:%.6f", longitude));
|
||||
tvLatitude.setText(String.format("当前纬度:%.6f", latitude));
|
||||
|
||||
// 2. 同步当前位置给Adapter(用于计算列表项实时距离)
|
||||
if (positionAdapter != null) {
|
||||
// 创建仅含经纬度的PositionModel(memo空,isEnable无需关注)
|
||||
PositionModel currentGpsPos = new PositionModel();
|
||||
currentGpsPos.setLongitude(longitude);
|
||||
currentGpsPos.setLatitude(latitude);
|
||||
// 调用Adapter方法传入当前GPS位置
|
||||
positionAdapter.setCurrentGpsPosition(currentGpsPos);
|
||||
}
|
||||
} else {
|
||||
// 位置为空(如GPS信号弱),显示等待提示
|
||||
tvLongitude.setText("当前经度:等待更新...");
|
||||
tvLatitude.setText("当前纬度:等待更新...");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化悬浮按钮(点击弹出备注输入框,添加当前位置到列表)
|
||||
*/
|
||||
private void initFabPButton() {
|
||||
fabPButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showLocationRemarkDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化RecyclerView和Adapter(绑定实时距离计算逻辑)
|
||||
*/
|
||||
private void initRecyclerViewAndAdapter() {
|
||||
// 1. 配置RecyclerView布局管理器(垂直列表)
|
||||
private void initRecyclerViewConfig() {
|
||||
// 显式 findViewById + 类型转换(Java 7 必须强转)
|
||||
mRecyclerView = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||
// 初始化线性布局管理器(垂直方向,默认)
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
rvPositionList.setLayoutManager(layoutManager);
|
||||
|
||||
// 2. 初始化Adapter(传入上下文和数据集合,兼容Java 7)
|
||||
PositionModel.loadBeanList(this, this.mPositionList, PositionModel.class);
|
||||
PositionTaskModel.loadBeanList(this, this.mPositionTasksList, PositionTaskModel.class);
|
||||
positionAdapter = new PositionAdapter(this, mPositionList, mPositionTasksList);
|
||||
rvPositionList.setAdapter(positionAdapter);
|
||||
|
||||
// 3. 设置Adapter删除监听(删除列表项并同步本地数据)
|
||||
positionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||
@Override
|
||||
public void onDeleteClick(int position) {
|
||||
showDeleteConfirmDialog(position);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 设置Adapter保存监听(编辑备注后同步本地数据)
|
||||
positionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||
@Override
|
||||
public void onSavePositionClick() {
|
||||
try {
|
||||
PositionModel.saveBeanList(LocationActivity.this, mPositionList, PositionModel.class);
|
||||
Toast.makeText(LocationActivity.this, "位置信息已保存", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(LocationActivity.this, "数据保存失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
positionAdapter.setOnSavePositionTaskClickListener(new PositionAdapter.OnSavePositionTaskClickListener() {
|
||||
@Override
|
||||
public void onSavePositionTaskClick() {
|
||||
try {
|
||||
PositionTaskModel.saveBeanList(LocationActivity.this, mPositionTasksList, PositionTaskModel.class);
|
||||
Toast.makeText(LocationActivity.this, "任务信息已保存", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(LocationActivity.this, "数据保存失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
mRecyclerView.setLayoutManager(layoutManager);
|
||||
// 固定列表大小(优化性能:避免列表项变化时重复测量RecyclerView)
|
||||
mRecyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从本地加载历史位置数据(基于BaseBean的持久化逻辑)
|
||||
* 初始化Adapter(核心:仅传上下文+服务实例,数据从服务获取)
|
||||
*/
|
||||
private void loadHistoryPositions() {
|
||||
try {
|
||||
ArrayList<PositionModel> historyList = new ArrayList<PositionModel>();
|
||||
// 调用PositionModel加载方法(读取本地保存的位置数据)
|
||||
PositionModel.loadBeanList(LocationActivity.this, historyList, PositionModel.class);
|
||||
if (historyList != null && !historyList.isEmpty()) {
|
||||
mPositionList.clear();
|
||||
mPositionList.addAll(historyList);
|
||||
positionAdapter.notifyDataSetChanged(); // 通知列表刷新
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 首次启动无数据时忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 弹出位置备注输入对话框(添加当前位置到列表)
|
||||
*/
|
||||
private void showLocationRemarkDialog() {
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(LocationActivity.this);
|
||||
dialogBuilder.setTitle("当前位置备注");
|
||||
|
||||
// 创建输入框(配置提示文本和内边距)
|
||||
final EditText remarkInput = new EditText(LocationActivity.this);
|
||||
remarkInput.setHint("请输入备注(如:公司/家/学校)");
|
||||
remarkInput.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
remarkInput.setPadding(
|
||||
dip2px(16),
|
||||
dip2px(8),
|
||||
dip2px(16),
|
||||
dip2px(8)
|
||||
);
|
||||
dialogBuilder.setView(remarkInput);
|
||||
|
||||
// 确定按钮:添加位置到列表 + 保存本地
|
||||
dialogBuilder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String inputRemark = remarkInput.getText().toString().trim();
|
||||
if (inputRemark.isEmpty()) {
|
||||
Toast.makeText(LocationActivity.this, "未输入备注", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验当前位置是否有效(避免无定位时添加空数据)
|
||||
if (currentLocation == null) {
|
||||
Toast.makeText(LocationActivity.this, "未获取到当前位置,请稍后再试", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加位置到列表(isEnableRealPositionDistance默认false,需手动开启)
|
||||
double longitude = currentLocation.getLongitude();
|
||||
double latitude = currentLocation.getLatitude();
|
||||
PositionModel newPosition = new PositionModel(
|
||||
PositionModel.genPositionId(),
|
||||
longitude,
|
||||
latitude,
|
||||
inputRemark,
|
||||
false // 默认不启用实时距离,用户可后续通过编辑开启
|
||||
);
|
||||
mPositionList.add(newPosition);
|
||||
|
||||
// 保存到本地 + 刷新列表
|
||||
try {
|
||||
PositionModel.saveBeanList(LocationActivity.this, mPositionList, PositionModel.class);
|
||||
positionAdapter.notifyItemInserted(mPositionList.size() - 1); // 局部刷新(性能更优)
|
||||
Toast.makeText(LocationActivity.this, "位置已添加", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(LocationActivity.this, "位置保存失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// 取消按钮:仅关闭对话框
|
||||
dialogBuilder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// 配置对话框(禁止外部点击关闭)
|
||||
dialogBuilder.setCancelable(false);
|
||||
AlertDialog remarkDialog = dialogBuilder.create();
|
||||
remarkDialog.show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示删除确认对话框(删除列表项)
|
||||
*/
|
||||
private void showDeleteConfirmDialog(final int position) {
|
||||
AlertDialog.Builder deleteDialogBuilder = new AlertDialog.Builder(this);
|
||||
deleteDialogBuilder.setTitle("删除位置记录")
|
||||
.setMessage("确定要删除这条位置吗?(删除后不可恢复)")
|
||||
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// 从列表移除 + 保存本地
|
||||
positionAdapter.removePosition(position);
|
||||
try {
|
||||
PositionModel.saveBeanList(LocationActivity.this, mPositionList, PositionModel.class);
|
||||
Toast.makeText(LocationActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(LocationActivity.this, "删除失败,请重试", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setCancelable(false);
|
||||
deleteDialogBuilder.show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查定位权限(仅精确定位权限,满足实时距离计算需求)
|
||||
*/
|
||||
private boolean checkLocationPermissions() {
|
||||
return ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 申请定位权限
|
||||
*/
|
||||
private void requestLocationPermissions() {
|
||||
String[] permissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
permissions,
|
||||
REQUEST_LOCATION_PERMISSIONS
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动实时定位(获取当前位置 + 监听位置变化)
|
||||
*/
|
||||
private void startRealTimeLocation() {
|
||||
if (!checkLocationPermissions()) {
|
||||
Toast.makeText(this, "定位权限未授予", Toast.LENGTH_SHORT).show();
|
||||
private void initAdapter() {
|
||||
// 前置校验:服务未绑定/服务实例为空,直接返回
|
||||
if (!isServiceBound || mDistanceService == null) {
|
||||
LogUtils.e("LocationActivity", "初始化Adapter失败:服务未绑定或实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先获取一次当前位置(初始化页面显示)
|
||||
fusedLocationClient.getLastLocation()
|
||||
.addOnSuccessListener(this, new OnSuccessListener<Location>() {
|
||||
// 1. 初始化Adapter(参数匹配:Context + DistanceServiceInterface)
|
||||
mAdapter = new PositionAdapter(this, mDistanceService);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
|
||||
// 2. 设置删除回调(通过服务执行删除,Adapter自动同步数据)
|
||||
mAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||
@Override
|
||||
public void onSuccess(Location location) {
|
||||
if (location != null) {
|
||||
currentLocation = location;
|
||||
tvLongitude.setText(String.format("当前经度:%.6f", location.getLongitude()));
|
||||
tvLatitude.setText(String.format("当前纬度:%.6f", location.getLatitude()));
|
||||
public void onDeleteClick(int position) {
|
||||
// 多重校验:服务状态+索引有效性
|
||||
if (isServiceBound && mDistanceService != null) {
|
||||
ArrayList<PositionModel> latestPosList = mDistanceService.getPositionList();
|
||||
if (position >= 0 && position < latestPosList.size()) {
|
||||
// 获取要删除的位置ID(从服务最新列表中取,避免本地数据过期)
|
||||
PositionModel targetPos = latestPosList.get(position);
|
||||
String posId = targetPos.getPositionId();
|
||||
// 调用服务删除方法(服务内部处理“删除位置+关联任务+清理可见位置”)
|
||||
mDistanceService.removePosition(posId);
|
||||
// 刷新Adapter(从服务获取最新列表,确保数据一致)
|
||||
mAdapter.updateAllPositions(mDistanceService.getPositionList());
|
||||
Toast.makeText(LocationActivity.this, "位置已删除(含关联任务)", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
LogUtils.w("LocationActivity", "删除失败:位置索引无效(" + position + ")");
|
||||
}
|
||||
} else {
|
||||
tvLongitude.setText("当前经度:等待更新...");
|
||||
tvLatitude.setText("当前纬度:等待更新...");
|
||||
Toast.makeText(LocationActivity.this, "删除失败:服务未绑定", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 注册位置监听器(实时更新位置)
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
locationRequest,
|
||||
locationCallback,
|
||||
getMainLooper() // 主线程更新UI,避免线程异常
|
||||
);
|
||||
// 3. 设置位置保存回调(保存逻辑由服务处理,此处仅提示)
|
||||
mAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||
@Override
|
||||
public void onSavePositionClick() {
|
||||
Toast.makeText(LocationActivity.this, "位置信息已保存(备注/距离开关)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 设置任务保存回调(同理,保存逻辑由服务处理)
|
||||
mAdapter.setOnSavePositionTaskClickListener(new PositionAdapter.OnSavePositionTaskClickListener() {
|
||||
@Override
|
||||
public void onSavePositionTaskClick() {
|
||||
Toast.makeText(LocationActivity.this, "任务信息已保存(新增/修改/删除)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------- 服务绑定/数据同步方法 ----------------------
|
||||
/**
|
||||
* 绑定DistanceRefreshService(自动创建服务,获取数据入口)
|
||||
*/
|
||||
private void bindDistanceService() {
|
||||
// Java 7 显式创建Intent(不使用方法链)
|
||||
Intent serviceIntent = new Intent(this, DistanceRefreshService.class);
|
||||
// 绑定服务:Context.BIND_AUTO_CREATE 表示“服务未启动则自动创建”
|
||||
bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理权限申请结果
|
||||
* 同步GPS位置到服务(供外部定位模块调用,如GPS回调)
|
||||
*/
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||
boolean isGranted = false;
|
||||
for (int result : grantResults) {
|
||||
if (result == PackageManager.PERMISSION_GRANTED) {
|
||||
isGranted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGranted) {
|
||||
startRealTimeLocation(); // 权限通过,启动定位
|
||||
} else {
|
||||
// 权限拒绝,提示并显示无权限状态
|
||||
Toast.makeText(this, "定位权限被拒绝,无法显示实时位置和距离", Toast.LENGTH_SHORT).show();
|
||||
tvLongitude.setText("当前经度:无权限");
|
||||
tvLatitude.setText("当前纬度:无权限");
|
||||
}
|
||||
public void syncGpsPositionToService(PositionModel gpsModel) {
|
||||
// 校验:服务绑定+GPS模型有效
|
||||
if (isServiceBound && mDistanceService != null && gpsModel != null) {
|
||||
mDistanceService.syncCurrentGpsPosition(gpsModel);
|
||||
// 可选:GPS更新后强制刷新一次距离(避免等待定时周期)
|
||||
mDistanceService.forceRefreshDistance();
|
||||
LogUtils.d("LocationActivity", "GPS位置已同步到服务,并触发即时距离计算");
|
||||
} else {
|
||||
LogUtils.w("LocationActivity", "同步GPS失败:服务未绑定或GPS模型无效");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------- 页面交互方法(新增位置按钮点击事件) ----------------------
|
||||
/**
|
||||
* 活动销毁:停止定位 + 停止距离刷新定时器(避免内存泄漏)
|
||||
* 新增位置(绑定到布局中“新增按钮”的 android:onClick="addNewPosition")
|
||||
*/
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
public void addNewPosition(View view) {
|
||||
// 1. 隐藏软键盘(避免新增时残留输入框焦点)
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null && getCurrentFocus() != null) {
|
||||
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
// 1. 停止定位监听(原逻辑不变)
|
||||
if (fusedLocationClient != null && locationCallback != null) {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback);
|
||||
}
|
||||
// 2. 校验服务状态(未绑定则提示)
|
||||
if (!isServiceBound || mDistanceService == null) {
|
||||
Toast.makeText(this, "新增失败:服务未绑定", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 关键:调用Adapter的stopTimer(内部已实现服务解绑,避免内存泄漏)
|
||||
if (positionAdapter != null) {
|
||||
positionAdapter.stopTimer(); // 此方法已重构为:解绑服务+清理资源
|
||||
}
|
||||
// 3. 创建示例位置模型(实际项目需替换为“用户输入经纬度/备注”)
|
||||
PositionModel newPos = new PositionModel();
|
||||
newPos.setPositionId(PositionModel.genPositionId()); // 静态方法生成唯一ID(需在PositionModel中实现)
|
||||
newPos.setLongitude(116.404267); // 示例经度(北京)
|
||||
newPos.setLatitude(39.915119); // 示例纬度
|
||||
newPos.setMemo("测试位置(可编辑备注)"); // 示例备注
|
||||
newPos.setIsSimpleView(true); // 默认显示“简单视图”(非编辑模式)
|
||||
newPos.setIsEnableRealPositionDistance(true); // 默认启用距离计算
|
||||
|
||||
// 3. 最后同步一次数据(原逻辑不变)
|
||||
try {
|
||||
if (mPositionList != null && !mPositionList.isEmpty()) {
|
||||
PositionModel.saveBeanList(this, mPositionList, PositionModel.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助工具:dp转px(适配不同屏幕分辨率)
|
||||
*/
|
||||
private int dip2px(float dpValue) {
|
||||
final float scale = getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f); // +0.5f用于四舍五入,确保精度
|
||||
// 4. 调用服务新增位置(服务内部去重+数据管理)
|
||||
mDistanceService.addPosition(newPos);
|
||||
// 5. 刷新Adapter(从服务获取最新列表,显示新增位置)
|
||||
mAdapter.updateAllPositions(mDistanceService.getPositionList());
|
||||
Toast.makeText(this, "新增位置成功(默认启用距离计算)", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.d("LocationActivity", "新增位置:ID=" + newPos.getPositionId() + ",备注=" + newPos.getMemo());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/01 04:50
|
||||
* @Describe AppConfigsModel
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import android.util.JsonWriter;
|
||||
import android.util.JsonReader;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AppConfigsModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "AppConfigsModel";
|
||||
|
||||
boolean isEnableDistanceRefreshService = false;
|
||||
|
||||
public AppConfigsModel(boolean isEnableDistanceRefreshService) {
|
||||
this.isEnableDistanceRefreshService = isEnableDistanceRefreshService;
|
||||
}
|
||||
|
||||
public void setIsEnableDistanceRefreshService(boolean isEnableDistanceRefreshService) {
|
||||
this.isEnableDistanceRefreshService = isEnableDistanceRefreshService;
|
||||
}
|
||||
|
||||
public boolean isEnableDistanceRefreshService() {
|
||||
return isEnableDistanceRefreshService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigsModel.class.getName();
|
||||
}
|
||||
|
||||
// JSON序列化(保存位置数据)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableDistanceRefreshService());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载位置数据,校验字段)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("isEnableDistanceRefreshService")) {
|
||||
setIsEnableDistanceRefreshService(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取位置数据
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -3,308 +3,458 @@ package cc.winboll.studio.positions.services;
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/30 19:53
|
||||
* @Describe DistanceRefreshService
|
||||
* @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
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.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.utils.NotificationUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||
|
||||
/**
|
||||
* 距离刷新服务:独立管理定时器,负责实时距离计算、任务触发判断、发送UI更新消息
|
||||
* 特性:1. 自启动(绑定后自动启动定时器) 2. 数据与Activity/Adapter同步 3. 本地消息通知UI更新
|
||||
* Java 7 适配:移除Lambda、Stream,使用匿名内部类+迭代器,明确泛型声明
|
||||
* 核心职责:
|
||||
* 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
|
||||
* 2. 单例式管理位置/任务数据,提供安全增删改查接口
|
||||
* 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
|
||||
* 4. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
|
||||
*/
|
||||
public class DistanceRefreshService extends Service {
|
||||
// 常量定义
|
||||
public class DistanceRefreshService extends Service implements PositionAdapter.DistanceServiceInterface {
|
||||
public static final String TAG = "DistanceRefreshService";
|
||||
public static final long REFRESH_INTERVAL = 5000; // 5秒刷新一次
|
||||
public static final int MSG_UPDATE_DISTANCE = 1001;
|
||||
public static final String KEY_POSITION_ID = "key_position_id";
|
||||
// 服务状态与配置
|
||||
private boolean isServiceRunning = false;
|
||||
private final ScheduledExecutorService distanceExecutor; // 定时计算线程池(单线程)
|
||||
private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
|
||||
|
||||
// 核心成员变量(Java7:明确泛型初始化)
|
||||
private Timer mDistanceTimer;
|
||||
private Handler mMainHandler;
|
||||
private PositionModel mCurrentGpsPosition;
|
||||
private ArrayList<PositionModel> mPositionList; // 持有Adapter传递的原列表引用
|
||||
private ArrayList<PositionTaskModel> mAllPositionTasks;
|
||||
private Map<String, Integer> mVisibleDistanceViewTags = new HashMap<String, Integer>();
|
||||
private OnDistanceUpdateReceiver mUpdateReceiver;
|
||||
private boolean isPositionListSynced = false; // 新增:标记位置列表是否已同步
|
||||
// 核心数据存储(服务内唯一数据源,避免外部直接修改)
|
||||
private final ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>();
|
||||
private final ArrayList<PositionTaskModel> mTaskList = new ArrayList<PositionTaskModel>();
|
||||
private final Set<String> mVisiblePositionIds = new HashSet<String>(); // 可见位置ID(优化性能)
|
||||
private PositionModel mCurrentGpsPosition; // 当前GPS位置(外部传入)
|
||||
|
||||
// 数据同步与消息接收接口(Activity/Adapter实现)
|
||||
public interface OnDistanceUpdateReceiver {
|
||||
void onDistanceUpdate(String positionId); // 简化:仅传递位置ID
|
||||
int getColorRes(int resId);
|
||||
// 服务绑定与UI回调
|
||||
private final IBinder mBinder = new DistanceBinder();
|
||||
private PositionAdapter.OnDistanceUpdateReceiver mDistanceReceiver; // Adapter回调接收器
|
||||
|
||||
// ---------------------- 构造初始化(线程池提前创建) ----------------------
|
||||
public DistanceRefreshService() {
|
||||
// Java 7 显式初始化线程池(单线程,避免并发修改数据)
|
||||
distanceExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
|
||||
// 服务绑定器(用于外部获取服务实例)
|
||||
// ---------------------- Binder 内部类(供外部绑定服务) ----------------------
|
||||
public class DistanceBinder extends Binder {
|
||||
/**
|
||||
* 外部绑定后获取服务实例(安全暴露服务引用)
|
||||
*/
|
||||
public DistanceRefreshService getService() {
|
||||
return DistanceRefreshService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private final IBinder mBinder = new DistanceBinder();
|
||||
|
||||
// ---------------------- 服务生命周期方法(严格管理资源) ----------------------
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.d(TAG, "DistanceRefreshService onCreate");
|
||||
// 初始化主线程Handler(Java7:匿名内部类实现handleMessage)
|
||||
mMainHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_UPDATE_DISTANCE && mUpdateReceiver != null) {
|
||||
// 解析消息:仅获取位置ID
|
||||
Bundle data = msg.getData();
|
||||
String positionId = data.getString(KEY_POSITION_ID);
|
||||
if (positionId != null) {
|
||||
LogUtils.d(TAG, "接收消息→转发更新:位置ID=" + positionId);
|
||||
mUpdateReceiver.onDistanceUpdate(positionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// 初始化数据集(Java7:明确泛型类型)
|
||||
mPositionList = new ArrayList<PositionModel>();
|
||||
mAllPositionTasks = new ArrayList<PositionTaskModel>();
|
||||
LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
|
||||
run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
run();
|
||||
AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
|
||||
boolean isEnableService = (bean == null) ? false : bean.isEnableDistanceRefreshService();
|
||||
return isEnableService ? Service.START_STICKY: super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
void run() {
|
||||
// 仅服务未运行时启动(避免重复启动)
|
||||
if (!isServiceRunning) {
|
||||
isServiceRunning = true;
|
||||
startDistanceRefreshTask(); // 启动定时距离计算
|
||||
LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒");
|
||||
} else {
|
||||
LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// 绑定服务时启动定时器(确保仅启动一次)
|
||||
if (mDistanceTimer == null) {
|
||||
startDistanceTimer();
|
||||
Log.d(TAG, "DistanceRefreshService onBind - 定时器首次启动");
|
||||
} else {
|
||||
Log.d(TAG, "DistanceRefreshService onBind - 定时器已在运行,无需重复启动");
|
||||
}
|
||||
return mBinder;
|
||||
LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + ")");
|
||||
return mBinder; // 返回Binder实例,供外部获取服务
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时器:核心逻辑(距离计算、任务触发、发送UI更新消息)
|
||||
* Java7:使用匿名内部类实现TimerTask,显式调用cancel+purge
|
||||
*/
|
||||
private void startDistanceTimer() {
|
||||
// 先停止旧定时器(避免残留任务)
|
||||
if (mDistanceTimer != null) {
|
||||
mDistanceTimer.cancel();
|
||||
mDistanceTimer.purge(); // 清空已取消的任务,释放资源
|
||||
}
|
||||
mDistanceTimer = new Timer();
|
||||
// Java7:匿名内部类实现TimerTask的run方法(替代Lambda)
|
||||
mDistanceTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "定时器触发→开始计算距离,位置列表同步状态=" + isPositionListSynced);
|
||||
calculateAndSendDistanceUpdates();
|
||||
checkAndTriggerTasks();
|
||||
}
|
||||
}, 0, REFRESH_INTERVAL); // 立即执行,之后每5秒执行一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心修改:计算距离并更新到mPositionList,仅发送位置ID通知
|
||||
* Java7:使用迭代器遍历,显式空判断
|
||||
*/
|
||||
private void calculateAndSendDistanceUpdates() {
|
||||
// 前置校验:位置列表未同步/为空/无GPS,直接返回
|
||||
if (!isPositionListSynced || mPositionList.isEmpty()) {
|
||||
LogUtils.d(TAG, "位置列表未同步/为空,跳过距离计算");
|
||||
return;
|
||||
}
|
||||
if (mCurrentGpsPosition == null) {
|
||||
LogUtils.d(TAG, "无当前GPS位置,跳过距离计算");
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历所有位置项,计算并设置realPositionDistance
|
||||
Iterator<PositionModel> positionIter = mPositionList.iterator();
|
||||
while (positionIter.hasNext()) {
|
||||
PositionModel targetModel = positionIter.next();
|
||||
String positionId = targetModel.getPositionId();
|
||||
|
||||
if (targetModel.isEnableRealPositionDistance()) {
|
||||
// 状态为true:计算距离(米),设置到realPositionDistance
|
||||
try {
|
||||
double distanceM = PositionModel.calculatePositionDistance(
|
||||
mCurrentGpsPosition, targetModel, false
|
||||
);
|
||||
targetModel.setRealPositionDistance(distanceM); // 存储计算结果到列表项
|
||||
LogUtils.d(TAG, "位置ID=" + positionId + " 计算距离:" + distanceM + "米");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 计算异常时,设置为-1(标记无效)
|
||||
targetModel.setRealPositionDistance(-1);
|
||||
LogUtils.e(TAG, "位置ID=" + positionId + " 距离计算异常:" + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 状态为false:强制设置realPositionDistance为-1
|
||||
targetModel.setRealPositionDistance(-1);
|
||||
LogUtils.d(TAG, "位置ID=" + positionId + " 未启用距离计算,设置距离为-1");
|
||||
}
|
||||
|
||||
// 发送更新通知(仅传位置ID)
|
||||
sendDistanceUpdateMessage(positionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务触发状态并发送通知
|
||||
* Java7:使用迭代器遍历任务列表(替代forEach Lambda)
|
||||
*/
|
||||
private void checkAndTriggerTasks() {
|
||||
if (mAllPositionTasks.isEmpty() || !isPositionListSynced || mPositionList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Java7:Iterator遍历ArrayList(替代forEach Lambda)
|
||||
Iterator<PositionTaskModel> taskIterator = mAllPositionTasks.iterator();
|
||||
while (taskIterator.hasNext()) {
|
||||
PositionTaskModel task = taskIterator.next();
|
||||
if (task.isBingo() && task.isEnable()) {
|
||||
NotificationUtils.show(getApplicationContext(), task.getTaskId(), task.getPositionId(), task.getTaskDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:判断任务触发状态(基于mPositionList中的realPositionDistance)
|
||||
* Java7:迭代器遍历任务列表,显式状态判断
|
||||
*/
|
||||
private void judgeTaskBingoStatus() {
|
||||
if (!isPositionListSynced || mPositionList.isEmpty() || mAllPositionTasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
PositionModel posModel = posIter.next();
|
||||
String posId = posModel.getPositionId();
|
||||
double distanceM = posModel.getRealPositionDistance();
|
||||
|
||||
// 遍历绑定当前位置的任务
|
||||
Iterator<PositionTaskModel> taskIter = mAllPositionTasks.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
PositionTaskModel task = taskIter.next();
|
||||
if (posId.equals(task.getPositionId()) && distanceM != -1) {
|
||||
boolean oldBingoState = task.isBingo();
|
||||
boolean newBingoState = false;
|
||||
|
||||
// 根据任务条件判断新状态
|
||||
if (task.isGreaterThan()) {
|
||||
newBingoState = task.isEnable() && distanceM > task.getDiscussDistance();
|
||||
} else if (task.isLessThan()) {
|
||||
newBingoState = task.isEnable() && distanceM < task.getDiscussDistance();
|
||||
}
|
||||
|
||||
// 仅状态变化时更新
|
||||
if (newBingoState != oldBingoState) {
|
||||
task.setIsBingo(newBingoState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化:仅发送位置ID通知(Adapter从mPositionList读取数据)
|
||||
* Java7:显式创建Message,避免obtainMessage链式调用
|
||||
*/
|
||||
private void sendDistanceUpdateMessage(String positionId) {
|
||||
Message msg = mMainHandler.obtainMessage(MSG_UPDATE_DISTANCE);
|
||||
Bundle data = new Bundle();
|
||||
data.putString(KEY_POSITION_ID, positionId);
|
||||
msg.setData(data);
|
||||
mMainHandler.sendMessage(msg);
|
||||
LogUtils.d(TAG, "发送更新通知:位置ID=" + positionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心修改:同步位置列表(持有原引用,不拷贝)
|
||||
*/
|
||||
public void syncPositionList(ArrayList<PositionModel> positionList) {
|
||||
if (positionList != null) {
|
||||
this.mPositionList = positionList; // 持有外部列表引用
|
||||
this.isPositionListSynced = true;
|
||||
LogUtils.d(TAG, "同步位置列表(持有引用):数量=" + positionList.size());
|
||||
} else {
|
||||
this.isPositionListSynced = false;
|
||||
LogUtils.w(TAG, "同步位置列表失败:传入列表为null");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 对外API(供Activity/Adapter调用) ----------------------
|
||||
public void setOnDistanceUpdateReceiver(OnDistanceUpdateReceiver receiver) {
|
||||
this.mUpdateReceiver = receiver;
|
||||
}
|
||||
|
||||
public void syncCurrentGpsPosition(PositionModel currentGpsPosition) {
|
||||
this.mCurrentGpsPosition = currentGpsPosition;
|
||||
LogUtils.d(TAG, "同步GPS位置:纬度=" + (currentGpsPosition != null ? currentGpsPosition.getLatitude() : 0.0f));
|
||||
}
|
||||
|
||||
public void syncAllPositionTasks(ArrayList<PositionTaskModel> allPositionTasks) {
|
||||
if (allPositionTasks != null) {
|
||||
this.mAllPositionTasks.clear();
|
||||
this.mAllPositionTasks.addAll(allPositionTasks);
|
||||
}
|
||||
}
|
||||
|
||||
public void addVisibleDistanceView(String positionId) {
|
||||
if (positionId != null && !mVisibleDistanceViewTags.containsKey(positionId)) {
|
||||
mVisibleDistanceViewTags.put(positionId, 0); // 用0占位,仅标记存在
|
||||
LogUtils.d(TAG, "添加可见位置ID:" + positionId + ",当前可见数量=" + mVisibleDistanceViewTags.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void removeVisibleDistanceView(String positionId) {
|
||||
if (positionId != null) {
|
||||
mVisibleDistanceViewTags.remove(positionId);
|
||||
LogUtils.d(TAG, "移除可见位置ID:" + positionId + ",当前可见数量=" + mVisibleDistanceViewTags.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void clearVisibleDistanceViews() {
|
||||
mVisibleDistanceViewTags.clear();
|
||||
LogUtils.d(TAG, "清空所有可见位置ID");
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
|
||||
// 解绑后清理资源,避免内存泄漏
|
||||
mDistanceReceiver = null;
|
||||
mVisiblePositionIds.clear();
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.d(TAG, "DistanceRefreshService onDestroy - 定时器销毁");
|
||||
// 销毁定时器,避免内存泄漏(Java7:显式判断非空)
|
||||
if (mDistanceTimer != null) {
|
||||
mDistanceTimer.cancel();
|
||||
mDistanceTimer.purge();
|
||||
}
|
||||
// 清空数据,解除引用(避免内存泄漏)
|
||||
mCurrentGpsPosition = null;
|
||||
mPositionList = null; // 释放列表引用
|
||||
mAllPositionTasks.clear();
|
||||
mVisibleDistanceViewTags.clear();
|
||||
mUpdateReceiver = null;
|
||||
isPositionListSynced = false;
|
||||
// 停止线程池+清空数据(服务销毁后释放资源)
|
||||
stopDistanceRefreshTask();
|
||||
clearAllData();
|
||||
isServiceRunning = false;
|
||||
LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放");
|
||||
}
|
||||
|
||||
// ---------------------- 核心:定时距离计算(后台线程+主线程回调) ----------------------
|
||||
/**
|
||||
* 启动定时距离计算任务(延迟1秒开始,周期执行)
|
||||
*/
|
||||
private void startDistanceRefreshTask() {
|
||||
if (distanceExecutor == null || distanceExecutor.isShutdown()) {
|
||||
LogUtils.e(TAG, "启动计算失败:线程池未初始化/已关闭");
|
||||
return;
|
||||
}
|
||||
|
||||
// Java 7:匿名内部类实现 Runnable(不使用Lambda)
|
||||
distanceExecutor.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 仅满足所有条件时计算:服务运行+GPS有效+有可见位置
|
||||
if (isServiceRunning && mCurrentGpsPosition != null && !mVisiblePositionIds.isEmpty()) {
|
||||
calculateVisiblePositionDistance();
|
||||
} else {
|
||||
// 打印跳过原因(便于调试)
|
||||
String reason = "";
|
||||
if (!isServiceRunning) reason = "服务未运行";
|
||||
else if (mCurrentGpsPosition == null) reason = "GPS无效";
|
||||
else if (mVisiblePositionIds.isEmpty()) reason = "无可见位置";
|
||||
LogUtils.d(TAG, "跳过距离计算:" + reason);
|
||||
}
|
||||
}
|
||||
}, 1, REFRESH_INTERVAL, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时计算任务(强制关闭线程池)
|
||||
*/
|
||||
private void stopDistanceRefreshTask() {
|
||||
if (distanceExecutor != null && !distanceExecutor.isShutdown()) {
|
||||
distanceExecutor.shutdownNow(); // 立即停止所有任务
|
||||
LogUtils.d(TAG, "距离计算任务已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算可见位置与GPS的距离(Haversine公式,后台线程执行)
|
||||
*/
|
||||
private void calculateVisiblePositionDistance() {
|
||||
// 拷贝可见ID集合:避免遍历中修改引发 ConcurrentModificationException
|
||||
Set<String> tempVisibleIds = new HashSet<String>(mVisiblePositionIds);
|
||||
if (tempVisibleIds.isEmpty()) return;
|
||||
|
||||
// Java 7:迭代器遍历位置列表(安全删除/修改)
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
PositionModel pos = posIter.next();
|
||||
String posId = pos.getPositionId();
|
||||
|
||||
// 仅计算“可见+启用距离”的位置
|
||||
if (tempVisibleIds.contains(posId) && pos.isEnableRealPositionDistance()) {
|
||||
try {
|
||||
// 调用Haversine公式计算距离(经纬度→米)
|
||||
double distanceM = calculateHaversineDistance(
|
||||
mCurrentGpsPosition.getLatitude(), mCurrentGpsPosition.getLongitude(),
|
||||
pos.getLatitude(), pos.getLongitude()
|
||||
);
|
||||
// 更新位置距离(服务内数据实时同步)
|
||||
pos.setRealPositionDistance(distanceM);
|
||||
LogUtils.d(TAG, "计算完成:位置ID=" + posId + ",距离=" + String.format("%.1f", distanceM) + "米");
|
||||
|
||||
// 回调Adapter更新UI(确保主线程)
|
||||
notifyDistanceUpdateToUI(posId);
|
||||
} catch (Exception e) {
|
||||
// 计算异常时标记距离为-1(Adapter识别为“计算异常”)
|
||||
pos.setRealPositionDistance(-1);
|
||||
notifyDistanceUpdateToUI(posId);
|
||||
LogUtils.e(TAG, "计算失败(位置ID=" + posId + "):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主线程回调Adapter更新UI(避免跨线程操作UI异常)
|
||||
*/
|
||||
private void notifyDistanceUpdateToUI(final String positionId) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// 已在主线程:直接回调
|
||||
if (mDistanceReceiver != null) {
|
||||
mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
}
|
||||
} else {
|
||||
// 子线程:通过主线程Handler切换(Java 7 显式创建Handler)
|
||||
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mDistanceReceiver != null) {
|
||||
mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Haversine公式:计算两点间直线距离(经纬度→米,精度满足日常需求)
|
||||
*/
|
||||
private double calculateHaversineDistance(double gpsLat, double gpsLon, double posLat, double posLon) {
|
||||
final double EARTH_RADIUS = 6371000; // 地球半径(米)
|
||||
double latDiff = Math.toRadians(posLat - gpsLat);
|
||||
double lonDiff = Math.toRadians(posLon - gpsLon);
|
||||
|
||||
double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
|
||||
+ Math.cos(Math.toRadians(gpsLat)) * Math.cos(Math.toRadians(posLat))
|
||||
* Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return EARTH_RADIUS * c; // 返回距离(米)
|
||||
}
|
||||
|
||||
// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
|
||||
@Override
|
||||
public ArrayList<PositionModel> getPositionList() {
|
||||
// 服务未运行返回空列表;运行中返回拷贝(避免外部修改原列表)
|
||||
if (!isServiceRunning) {
|
||||
LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
|
||||
return new ArrayList<PositionModel>();
|
||||
}
|
||||
return new ArrayList<PositionModel>(mPositionList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<PositionTaskModel> getPositionTasksList() {
|
||||
if (!isServiceRunning) {
|
||||
LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
|
||||
return new ArrayList<PositionTaskModel>();
|
||||
}
|
||||
return new ArrayList<PositionTaskModel>(mTaskList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncCurrentGpsPosition(PositionModel position) {
|
||||
if (position == null) {
|
||||
LogUtils.w(TAG, "syncCurrentGpsPosition:GPS位置为空,同步失败");
|
||||
return;
|
||||
}
|
||||
this.mCurrentGpsPosition = position;
|
||||
LogUtils.d(TAG, "syncCurrentGpsPosition:同步成功(纬度=" + position.getLatitude() + ",经度=" + position.getLongitude() + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
|
||||
this.mDistanceReceiver = receiver;
|
||||
LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addVisibleDistanceView(String positionId) {
|
||||
if (!isServiceRunning || positionId == null) {
|
||||
LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
|
||||
return;
|
||||
}
|
||||
if (mVisiblePositionIds.add(positionId)) {
|
||||
LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeVisibleDistanceView(String positionId) {
|
||||
if (positionId == null) {
|
||||
LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
|
||||
return;
|
||||
}
|
||||
if (mVisiblePositionIds.remove(positionId)) {
|
||||
LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearVisibleDistanceViews() {
|
||||
mVisiblePositionIds.clear();
|
||||
LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
|
||||
}
|
||||
|
||||
// ---------------------- 数据管理接口(给外部/Adapter调用,安全校验) ----------------------
|
||||
/**
|
||||
* 获取服务运行状态(供Activity/Adapter判断是否加载数据)
|
||||
*/
|
||||
public boolean isServiceRunning() {
|
||||
return isServiceRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加位置(Adapter新增位置时调用,自动去重)
|
||||
*/
|
||||
public void addPosition(PositionModel position) {
|
||||
if (!isServiceRunning || position == null || position.getPositionId() == null) {
|
||||
LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 去重校验(根据位置ID)
|
||||
boolean isDuplicate = false;
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
if (position.getPositionId().equals(posIter.next().getPositionId())) {
|
||||
isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
mPositionList.add(position);
|
||||
LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + ")");
|
||||
} else {
|
||||
LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除位置(连带删除关联任务,Adapter删除时调用)
|
||||
*/
|
||||
public void removePosition(String positionId) {
|
||||
if (!isServiceRunning || positionId == null) {
|
||||
LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 删除位置
|
||||
boolean isRemoved = false;
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
PositionModel pos = posIter.next();
|
||||
if (positionId.equals(pos.getPositionId())) {
|
||||
posIter.remove();
|
||||
isRemoved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRemoved) {
|
||||
// 2. 删除关联任务
|
||||
Iterator<PositionTaskModel> taskIter = mTaskList.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
if (positionId.equals(taskIter.next().getPositionId())) {
|
||||
taskIter.remove();
|
||||
}
|
||||
}
|
||||
// 3. 移除可见位置(避免继续计算已删除位置)
|
||||
mVisiblePositionIds.remove(positionId);
|
||||
LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ")");
|
||||
} else {
|
||||
LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新位置信息(Adapter编辑后调用,仅更新备注/距离开关)
|
||||
*/
|
||||
public void updatePosition(PositionModel updatedPosition) {
|
||||
if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
|
||||
LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isUpdated = false;
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
PositionModel pos = posIter.next();
|
||||
if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
|
||||
// 仅更新允许修改的字段(备注+距离开关)
|
||||
pos.setMemo(updatedPosition.getMemo());
|
||||
pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
|
||||
// 关闭距离时重置距离值
|
||||
if (!updatedPosition.isEnableRealPositionDistance()) {
|
||||
pos.setRealPositionDistance(-1);
|
||||
notifyDistanceUpdateToUI(pos.getPositionId()); // 通知UI更新状态
|
||||
}
|
||||
isUpdated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + ")");
|
||||
} else {
|
||||
LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步任务列表(Adapter编辑任务后调用,全量覆盖+去重)
|
||||
*/
|
||||
public void syncAllPositionTasks(ArrayList<PositionTaskModel> tasks) {
|
||||
if (!isServiceRunning || tasks == null) {
|
||||
LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 清空旧任务(全量同步,避免增量逻辑复杂)
|
||||
mTaskList.clear();
|
||||
// 2. 添加新任务(根据任务ID去重,避免重复)
|
||||
Set taskIdSet = new HashSet();
|
||||
Iterator taskIter = tasks.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||
if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
|
||||
taskIdSet.add(task.getTaskId());
|
||||
mTaskList.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据(服务销毁/调试时调用,重置所有状态)
|
||||
*/
|
||||
public void clearAllData() {
|
||||
mPositionList.clear();
|
||||
mTaskList.clear();
|
||||
mVisiblePositionIds.clear();
|
||||
mCurrentGpsPosition = null;
|
||||
LogUtils.d(TAG, "clearAllData:所有数据已清空(位置/任务/GPS/可见位置)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新距离(外部主动触发,如GPS位置突变时)
|
||||
*/
|
||||
public void forceRefreshDistance() {
|
||||
if (!isServiceRunning) {
|
||||
LogUtils.w(TAG, "forceRefreshDistance:服务未运行,刷新失败");
|
||||
return;
|
||||
}
|
||||
if (distanceExecutor != null && !distanceExecutor.isShutdown()) {
|
||||
// 提交即时任务(不等待定时周期)
|
||||
distanceExecutor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
calculateVisiblePositionDistance();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "forceRefreshDistance:已触发即时距离计算");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,39 +6,38 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
<!-- 顶部 Toolbar -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
||||
|
||||
<!-- 服务控制开关 -->
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_service_control"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="位置服务"/>
|
||||
|
||||
<!-- 跳转按钮:位置管理页 -->
|
||||
<Button
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
android:layout_margin="16dp"
|
||||
android:onClick="onPositions"
|
||||
android:text="进入位置管理"/>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<!-- 跳转按钮:日志页 -->
|
||||
<Button
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center_vertical|center_horizontal">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Positions"
|
||||
android:onClick="onPositions"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Log"
|
||||
android:onClick="onLog"/>
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:onClick="onLog"
|
||||
android:text="查看操作日志"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -43,17 +43,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#333333"
|
||||
android:id="@+id/tv_edit_real_distance"/>
|
||||
android:id="@+id/tv_edit_distance"/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_real_distance_switch"
|
||||
android:id="@+id/rg_distance_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginLeft="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_disable"
|
||||
android:id="@+id/rb_distance_disable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="禁用"
|
||||
@@ -61,7 +61,7 @@
|
||||
android:checked="true"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_enable"
|
||||
android:id="@+id/rb_distance_enable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="启用"
|
||||
@@ -109,7 +109,7 @@
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_edit_confirm"
|
||||
android:id="@+id/btn_edit_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/btn_confirm_bg"
|
||||
@@ -140,6 +140,14 @@
|
||||
android:textSize="14sp"
|
||||
android:textColor="#333333"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#333333"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
16
positions/src/main/res/layout/item_position_empty.xml
Normal file
16
positions/src/main/res/layout/item_position_empty.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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="100dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_empty_tip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/colorGrayText"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_simple_real_distance"
|
||||
android:id="@+id/tv_simple_distance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
|
||||
Reference in New Issue
Block a user