后台服务版初版

This commit is contained in:
ZhanGSKen
2025-10-01 05:20:46 +08:00
parent 80424c523d
commit cf6209e2b4
11 changed files with 1491 additions and 1559 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #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 stageCount=4
libraryProject= libraryProject=
baseVersion=15.0 baseVersion=15.0
publishVersion=15.0.3 publishVersion=15.0.3
buildCount=10 buildCount=19
baseBetaVersion=15.0.4 baseBetaVersion=15.0.4

View File

@@ -12,6 +12,9 @@
<!-- 拥有完全的网络访问权限 --> <!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- 在后台使用位置信息 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-feature <uses-feature
android:name="android.hardware.location.gps" android:name="android.hardware.location.gps"
android:required="false"/> android:required="false"/>

View File

@@ -1,32 +1,186 @@
package cc.winboll.studio.positions; package cc.winboll.studio.positions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.View; import android.view.View;
import android.widget.CompoundButton;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogActivity; import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.positions.activities.LocationActivity; import cc.winboll.studio.positions.activities.LocationActivity;
import cc.winboll.studio.positions.services.DistanceRefreshService;
import com.hjq.toast.ToastUtils; 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 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main); // 关联主页面布局
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar); // 1. 初始化顶部 Toolbar保留原逻辑设置页面标题
setSupportActionBar(toolbar); initToolbar();
// 2. 初始化服务控制开关核心功能绑定开关点击事件、读取SP状态
initServiceSwitch();
// 3. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
bindDistanceService();
} }
@Override
protected void onDestroy() {
super.onDestroy();
// 页面销毁时解绑服务避免Activity与服务相互引用导致内存泄漏
if (isServiceBound) {
unbindService(mServiceConn);
isServiceBound = false;
mDistanceService = null;
}
}
// ---------------------- 核心功能1初始化UI组件Toolbar + 服务开关) ----------------------
/**
* 初始化顶部 Toolbar设置页面标题
*/
private void initToolbar() {
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
setSupportActionBar(mToolbar);
// 给ActionBar设置标题先判断非空避免空指针异常
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle("位置管理");
}
}
/**
* 初始化服务控制开关读取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) { public void onPositions(View view) {
startActivity(new Intent(this, LocationActivity.class)); // 从配置文件读取服务状态(避免依赖服务绑定,提升稳定性)
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) { public void onLog(View view) {
LogActivity.startLogActivity(this); LogActivity.startLogActivity(this); // 调用LogActivity静态方法跳转保留原逻辑
} }
} }

View File

@@ -3,443 +3,251 @@ package cc.winboll.studio.positions.activities;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 18:22 * @Date 2025/09/29 18:22
* @Describe 当前位置实时显示 + 位置列表实时距离计算 * @Describe 位置列表页面Java 7 兼容完全依赖DistanceRefreshService数据
*/ */
import android.Manifest; import android.content.ComponentName;
import android.app.AlertDialog; import android.content.Context;
import android.content.DialogInterface; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.ServiceConnection;
import android.location.Location; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.text.InputType; import android.os.IBinder;
import android.view.View; import android.view.View;
import android.widget.Button; import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.positions.R; import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.adapters.PositionAdapter; import cc.winboll.studio.positions.adapters.PositionAdapter;
import cc.winboll.studio.positions.models.PositionModel; import cc.winboll.studio.positions.models.PositionModel;
import com.google.android.gms.location.FusedLocationProviderClient; import cc.winboll.studio.positions.services.DistanceRefreshService;
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 java.util.ArrayList; import java.util.ArrayList;
import cc.winboll.studio.positions.models.PositionTaskModel;
/** /**
* 实时定位活动窗口 * 核心逻辑
* 1. 申请定位必需权限(精确定位) * 1. 启动前检查服务状态,未运行则拦截并提示
* 2. 初始化FusedLocationProviderClient谷歌官方定位服务兼容所有安卓版本 * 2. 绑定服务后通过接口获取数据,不本地存储数据
* 3. 实时监听位置变化,更新显示经度、纬度 + 同步给Adapter计算实时距离 * 3. Adapter初始化仅传上下文+服务实例,数据从服务实时获取
* 4. 右下角圆形悬浮按钮含大写P字母支持添加位置 * 4. 严格Java 7语法显式类型转换、匿名内部类、无Lambda
* 5. 位置列表支持实时距离显示按isEnableRealPositionDistance控制
*/ */
public class LocationActivity extends AppCompatActivity { public class LocationActivity extends AppCompatActivity {
public static final String TAG = "LocationActivity"; public static final String TAG = "LocationActivity";
// 1. 核心组件与常量定义兼容Java 7移除不必要final // SP配置常量与服务保持一致用于判断服务状态
private static final int REQUEST_LOCATION_PERMISSIONS = 1004; // 定位权限请求码 private static final String SP_SERVICE_CONFIG = "service_config";
private FusedLocationProviderClient fusedLocationClient; // 定位核心客户端 private static final String KEY_SERVICE_RUNNING = "is_service_running";
private LocationCallback locationCallback; // 位置变化监听器 // 页面核心控件与变量
private LocationRequest locationRequest; // 定位请求配置(频率、精度等) private RecyclerView mRecyclerView;
private Location currentLocation; // 存储当前最新位置(用于同步给Adapter private PositionAdapter mAdapter;
private DistanceRefreshService mDistanceService; // 服务实例已实现DistanceServiceInterface
private boolean isServiceBound = false; // 服务绑定状态标记
// UI控件Java 7显式声明+强制转换) // ---------------------- 服务连接Java 7 匿名内部类实现) ----------------------
private TextView tvLongitude; // 经度显示 private final ServiceConnection mServiceConn = new ServiceConnection() {
private TextView tvLatitude; // 纬度显示 @Override
private Button fabPButton; // 右下角圆形悬浮按钮P字母 public void onServiceConnected(ComponentName name, IBinder service) {
private RecyclerView rvPositionList; // 位置列表RecyclerView // 显式类型转换Java 7 不支持自动推断,必须强转
private PositionAdapter positionAdapter; // 列表Adapter含实时距离逻辑 DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>(); // 位置数据集合 mDistanceService = binder.getService();
ArrayList<PositionTaskModel> mPositionTasksList = new ArrayList<PositionTaskModel>(); // 位置数据集合 isServiceBound = true;
LogUtils.d("LocationActivity", "服务绑定成功开始初始化Adapter");
// 绑定成功后初始化Adapter仅传上下文+服务实例)
initAdapter();
}
@Override
public void onServiceDisconnected(ComponentName name) {
// 服务意外断开:置空引用+更新状态,避免空指针
mDistanceService = null;
isServiceBound = false;
LogUtils.w("LocationActivity", "服务意外断开连接(可能被系统回收)");
Toast.makeText(LocationActivity.this, "服务已断开,请重新进入页面", Toast.LENGTH_SHORT).show();
}
};
// ---------------------- 页面生命周期(严格管理服务绑定/资源) ----------------------
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_location); setContentView(R.layout.activity_location);
// 绑定UI控件Java 7显式强制转换 // 1. 优先检查服务状态:未运行则提示并关闭页面
tvLongitude = (TextView) findViewById(R.id.tv_longitude); checkServiceRunningStatus();
tvLatitude = (TextView) findViewById(R.id.tv_latitude); // 2. 初始化RecyclerView基础配置+性能优化)
fabPButton = (Button) findViewById(R.id.fab_p_button); initRecyclerViewConfig();
rvPositionList = (RecyclerView) findViewById(R.id.rv_position_list); // 3. 绑定服务(获取数据的唯一入口,自动创建服务)
bindDistanceService();
// 初始化核心逻辑定位配置→悬浮按钮→列表Adapter→加载历史数据
initLocationConfig();
initFabPButton();
initRecyclerViewAndAdapter();
loadHistoryPositions();
// 检查并申请定位权限(权限通过后启动实时定位)
if (checkLocationPermissions()) {
startRealTimeLocation();
} else {
requestLocationPermissions();
}
} }
/**
* 初始化定位配置兼容Java 7用LocationRequest.create()替代Builder
*/
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) {
// 创建仅含经纬度的PositionModelmemo空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布局管理器垂直列表
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();
}
}
});
}
/**
* 从本地加载历史位置数据基于BaseBean的持久化逻辑
*/
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();
return;
}
// 1. 先获取一次当前位置(初始化页面显示)
fusedLocationClient.getLastLocation()
.addOnSuccessListener(this, new OnSuccessListener<Location>() {
@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()));
} else {
tvLongitude.setText("当前经度:等待更新...");
tvLatitude.setText("当前纬度:等待更新...");
}
}
});
// 2. 注册位置监听器(实时更新位置)
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
getMainLooper() // 主线程更新UI避免线程异常
);
}
/**
* 处理权限申请结果
*/
@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("当前纬度:无权限");
}
}
}
/**
* 活动销毁:停止定位 + 停止距离刷新定时器(避免内存泄漏)
*/
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
// 1. 解绑服务(仅绑定状态下执行,避免异常)
// 1. 停止定位监听(原逻辑不变) if (isServiceBound) {
if (fusedLocationClient != null && locationCallback != null) { unbindService(mServiceConn);
fusedLocationClient.removeLocationUpdates(locationCallback); isServiceBound = false;
mDistanceService = null; // 置空引用帮助GC回收
LogUtils.d("LocationActivity", "服务已解绑,避免内存泄漏");
}
// 2. 清理Adapter资源调用Adapter内部销毁方法
if (mAdapter != null) {
mAdapter.release();
mAdapter = null;
}
// 3. 清理RecyclerView引用
mRecyclerView = null;
} }
// 2. 关键调用Adapter的stopTimer内部已实现服务解绑避免内存泄漏 // ---------------------- 核心初始化方法(服务状态检查+RecyclerView+Adapter ----------------------
if (positionAdapter != null) { /**
positionAdapter.stopTimer(); // 此方法已重构为:解绑服务+清理资源 * 检查服务运行状态从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);
// 3. 最后同步一次数据(原逻辑不变) if (!isServiceRunning) {
try { // 服务未运行:提示用户并关闭页面
if (mPositionList != null && !mPositionList.isEmpty()) { Toast.makeText(this, "请先启动位置服务,否则无法加载数据", Toast.LENGTH_SHORT).show();
PositionModel.saveBeanList(this, mPositionList, PositionModel.class); finish(); // 关闭当前页面,返回上一级
}
} catch (Exception e) {
e.printStackTrace();
} }
} }
/** /**
* 辅助工具dp转px适配不同屏幕分辨率 * 初始化RecyclerView布局管理器+性能优化
*/ */
private int dip2px(float dpValue) { private void initRecyclerViewConfig() {
final float scale = getResources().getDisplayMetrics().density; // 显式 findViewById + 类型转换Java 7 必须强转)
return (int) (dpValue * scale + 0.5f); // +0.5f用于四舍五入,确保精度 mRecyclerView = (RecyclerView) findViewById(R.id.rv_position_list);
// 初始化线性布局管理器(垂直方向,默认)
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
// 固定列表大小优化性能避免列表项变化时重复测量RecyclerView
mRecyclerView.setHasFixedSize(true);
}
/**
* 初始化Adapter核心仅传上下文+服务实例,数据从服务获取)
*/
private void initAdapter() {
// 前置校验:服务未绑定/服务实例为空,直接返回
if (!isServiceBound || mDistanceService == null) {
LogUtils.e("LocationActivity", "初始化Adapter失败服务未绑定或实例为空");
return;
}
// 1. 初始化Adapter参数匹配Context + DistanceServiceInterface
mAdapter = new PositionAdapter(this, mDistanceService);
mRecyclerView.setAdapter(mAdapter);
// 2. 设置删除回调通过服务执行删除Adapter自动同步数据
mAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
@Override
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 {
Toast.makeText(LocationActivity.this, "删除失败:服务未绑定", Toast.LENGTH_SHORT).show();
}
}
});
// 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回调
*/
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"
*/
public void addNewPosition(View view) {
// 1. 隐藏软键盘(避免新增时残留输入框焦点)
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null && getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
// 2. 校验服务状态(未绑定则提示)
if (!isServiceBound || mDistanceService == null) {
Toast.makeText(this, "新增失败:服务未绑定", Toast.LENGTH_SHORT).show();
return;
}
// 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); // 默认启用距离计算
// 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());
} }
} }

View File

@@ -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;
}
}

View File

@@ -3,308 +3,458 @@ package cc.winboll.studio.positions.services;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/30 19:53 * @Date 2025/09/30 19:53
* @Describe DistanceRefreshService * @Describe 位置距离服务:管理数据+定时计算距离+适配AdapterJava 7 兼容)
*/ */
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.os.Message;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils; 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.PositionModel;
import cc.winboll.studio.positions.models.PositionTaskModel; import cc.winboll.studio.positions.models.PositionTaskModel;
import cc.winboll.studio.positions.utils.NotificationUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Set;
import java.util.Timer; import java.util.concurrent.Executors;
import java.util.TimerTask; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import cc.winboll.studio.positions.models.AppConfigsModel;
/** /**
* 距离刷新服务独立管理定时器负责实时距离计算、任务触发判断、发送UI更新消息 * 核心职责:
* 特性1. 自启动(绑定后自动启动定时器) 2. 数据与Activity/Adapter同步 3. 本地消息通知UI更新 * 1. 实现 PositionAdapter.DistanceServiceInterface 接口解耦Adapter与服务
* Java 7 适配移除Lambda、Stream使用匿名内部类+迭代器,明确泛型声明 * 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 String TAG = "DistanceRefreshService";
public static final long REFRESH_INTERVAL = 5000; // 5秒刷新一次 // 服务状态与配置
public static final int MSG_UPDATE_DISTANCE = 1001; private boolean isServiceRunning = false;
public static final String KEY_POSITION_ID = "key_position_id"; private final ScheduledExecutorService distanceExecutor; // 定时计算线程池(单线程)
private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
// 核心成员变量Java7明确泛型初始化 // 核心数据存储(服务内唯一数据源,避免外部直接修改
private Timer mDistanceTimer; private final ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>();
private Handler mMainHandler; private final ArrayList<PositionTaskModel> mTaskList = new ArrayList<PositionTaskModel>();
private PositionModel mCurrentGpsPosition; private final Set<String> mVisiblePositionIds = new HashSet<String>(); // 可见位置ID优化性能
private ArrayList<PositionModel> mPositionList; // 持有Adapter传递的原列表引用 private PositionModel mCurrentGpsPosition; // 当前GPS位置外部传入
private ArrayList<PositionTaskModel> mAllPositionTasks;
private Map<String, Integer> mVisibleDistanceViewTags = new HashMap<String, Integer>();
private OnDistanceUpdateReceiver mUpdateReceiver;
private boolean isPositionListSynced = false; // 新增:标记位置列表是否已同步
// 数据同步与消息接收接口Activity/Adapter实现 // 服务绑定与UI回调
public interface OnDistanceUpdateReceiver { private final IBinder mBinder = new DistanceBinder();
void onDistanceUpdate(String positionId); // 简化仅传递位置ID private PositionAdapter.OnDistanceUpdateReceiver mDistanceReceiver; // Adapter回调接收器
int getColorRes(int resId);
// ---------------------- 构造初始化(线程池提前创建) ----------------------
public DistanceRefreshService() {
// Java 7 显式初始化线程池(单线程,避免并发修改数据)
distanceExecutor = Executors.newSingleThreadScheduledExecutor();
} }
// 服务绑定器(用于外部获取服务实例) // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
public class DistanceBinder extends Binder { public class DistanceBinder extends Binder {
/**
* 外部绑定后获取服务实例(安全暴露服务引用)
*/
public DistanceRefreshService getService() { public DistanceRefreshService getService() {
return DistanceRefreshService.this; return DistanceRefreshService.this;
} }
} }
private final IBinder mBinder = new DistanceBinder(); // ---------------------- 服务生命周期方法(严格管理资源) ----------------------
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Log.d(TAG, "DistanceRefreshService onCreate"); LogUtils.d(TAG, "服务 onCreate初始化完成等待启动命令");
// 初始化主线程HandlerJava7匿名内部类实现handleMessage run();
mMainHandler = new Handler(Looper.getMainLooper()) { }
@Override @Override
public void handleMessage(Message msg) { public int onStartCommand(Intent intent, int flags, int startId) {
super.handleMessage(msg); run();
if (msg.what == MSG_UPDATE_DISTANCE && mUpdateReceiver != null) { AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
// 解析消息仅获取位置ID boolean isEnableService = (bean == null) ? false : bean.isEnableDistanceRefreshService();
Bundle data = msg.getData(); return isEnableService ? Service.START_STICKY: super.onStartCommand(intent, flags, startId);
String positionId = data.getString(KEY_POSITION_ID);
if (positionId != null) {
LogUtils.d(TAG, "接收消息→转发更新位置ID=" + positionId);
mUpdateReceiver.onDistanceUpdate(positionId);
} }
void run() {
// 仅服务未运行时启动(避免重复启动)
if (!isServiceRunning) {
isServiceRunning = true;
startDistanceRefreshTask(); // 启动定时距离计算
LogUtils.d(TAG, "服务 onStartCommand启动成功刷新间隔=" + REFRESH_INTERVAL + "");
} else {
LogUtils.w(TAG, "服务 onStartCommand已在运行无需重复启动");
} }
} }
};
// 初始化数据集Java7明确泛型类型
mPositionList = new ArrayList<PositionModel>();
mAllPositionTasks = new ArrayList<PositionTaskModel>();
}
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
// 绑定服务时启动定时器(确保仅启动一次) LogUtils.d(TAG, "服务 onBind外部绑定成功运行状态" + (isServiceRunning ? "" : "") + "");
if (mDistanceTimer == null) { return mBinder; // 返回Binder实例供外部获取服务
startDistanceTimer();
Log.d(TAG, "DistanceRefreshService onBind - 定时器首次启动");
} else {
Log.d(TAG, "DistanceRefreshService onBind - 定时器已在运行,无需重复启动");
}
return mBinder;
} }
/**
* 启动定时器核心逻辑距离计算、任务触发、发送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 @Override
public void run() { public boolean onUnbind(Intent intent) {
LogUtils.d(TAG, "定时器触发→开始计算距离,位置列表同步状态=" + isPositionListSynced); LogUtils.d(TAG, "服务 onUnbind外部解绑清理回调与可见位置");
calculateAndSendDistanceUpdates(); // 解绑后清理资源,避免内存泄漏
checkAndTriggerTasks(); mDistanceReceiver = null;
} mVisiblePositionIds.clear();
}, 0, REFRESH_INTERVAL); // 立即执行之后每5秒执行一次 return super.onUnbind(intent);
}
/**
* 核心修改计算距离并更新到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;
}
// Java7Iterator遍历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 @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
Log.d(TAG, "DistanceRefreshService onDestroy - 定时器销毁"); // 停止线程池+清空数据(服务销毁后释放资源)
// 销毁定时器避免内存泄漏Java7显式判断非空 stopDistanceRefreshTask();
if (mDistanceTimer != null) { clearAllData();
mDistanceTimer.cancel(); isServiceRunning = false;
mDistanceTimer.purge(); 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) {
// 计算异常时标记距离为-1Adapter识别为“计算异常”
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, "syncCurrentGpsPositionGPS位置为空同步失败");
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已触发即时距离计算");
} }
// 清空数据,解除引用(避免内存泄漏)
mCurrentGpsPosition = null;
mPositionList = null; // 释放列表引用
mAllPositionTasks.clear();
mVisibleDistanceViewTags.clear();
mUpdateReceiver = null;
isPositionListSynced = false;
} }
} }

View File

@@ -6,39 +6,38 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout <!-- 顶部 Toolbar -->
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
</com.google.android.material.appbar.AppBarLayout> <!-- 服务控制开关 -->
<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="位置服务"/>
<LinearLayout <!-- 跳转按钮:位置管理页 -->
android:orientation="vertical" <Button
android:layout_width="match_parent" 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:layout_height="wrap_content"
android:text="Positions" android:layout_margin="16dp"
android:onClick="onPositions"/> android:onClick="onPositions"
android:text="进入位置管理"/>
<!-- 跳转按钮:日志页 -->
<Button <Button
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Log" android:layout_margin="16dp"
android:onClick="onLog"/> android:onClick="onLog"
android:text="查看操作日志"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -43,17 +43,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="14sp"
android:textColor="#333333" android:textColor="#333333"
android:id="@+id/tv_edit_real_distance"/> android:id="@+id/tv_edit_distance"/>
<RadioGroup <RadioGroup
android:id="@+id/rg_real_distance_switch" android:id="@+id/rg_distance_switch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginLeft="8dp"> android:layout_marginLeft="8dp">
<RadioButton <RadioButton
android:id="@+id/rb_disable" android:id="@+id/rb_distance_disable"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="禁用" android:text="禁用"
@@ -61,7 +61,7 @@
android:checked="true"/> android:checked="true"/>
<RadioButton <RadioButton
android:id="@+id/rb_enable" android:id="@+id/rb_distance_enable"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="启用" android:text="启用"
@@ -109,7 +109,7 @@
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<Button <Button
android:id="@+id/btn_edit_confirm" android:id="@+id/btn_edit_save"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="32dp" android:layout_height="32dp"
android:background="@drawable/btn_confirm_bg" android:background="@drawable/btn_confirm_bg"
@@ -140,6 +140,14 @@
android:textSize="14sp" android:textSize="14sp"
android:textColor="#333333"/> 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 <View
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"

View 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>

View File

@@ -29,7 +29,7 @@
android:layout_marginTop="4dp"/> android:layout_marginTop="4dp"/>
<TextView <TextView
android:id="@+id/tv_simple_real_distance" android:id="@+id/tv_simple_distance"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="14sp"