后台服务版初版
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"; | ||||
| 	 | ||||
|     // 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) | ||||
| 	// 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; // 服务绑定状态标记 | ||||
|  | ||||
|     // 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>(); // 位置数据集合 | ||||
|     // ---------------------- 服务连接(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(); | ||||
|         } | ||||
|  | ||||
|         @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"> | ||||
| 		android:layout_height="wrap_content" | ||||
| 		android:layout_margin="16dp" | ||||
| 		android:onClick="onLog" | ||||
| 		android:text="查看操作日志"/> | ||||
| 	 | ||||
| 		<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> | ||||
|  | ||||
| </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
	 ZhanGSKen
					ZhanGSKen