diff --git a/app/src/main/java/moe/fuqiuluo/portal/MainActivity.kt b/app/src/main/java/moe/fuqiuluo/portal/MainActivity.kt index 54b5fed..04560b0 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/MainActivity.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/MainActivity.kt @@ -77,6 +77,7 @@ import moe.fuqiuluo.portal.bdmap.toPoi import moe.fuqiuluo.portal.databinding.ActivityMainBinding import moe.fuqiuluo.portal.ext.gcj02 import moe.fuqiuluo.portal.ext.wgs84 +import moe.fuqiuluo.portal.ext.sharedPrefs import moe.fuqiuluo.portal.ui.notification.NotificationUtils import moe.fuqiuluo.portal.ui.viewmodel.BaiduMapViewModel import moe.fuqiuluo.portal.ui.viewmodel.MockServiceViewModel @@ -166,6 +167,9 @@ class MainActivity : AppCompatActivity() { Toast.makeText(this, "无Root可能导致传感器Hook失效", Toast.LENGTH_LONG).show() } + // One-time migration of historical locations from StringSet to JSON format + migrateHistoricalLocationsToJson() + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { if(checkPermission()) { @@ -466,6 +470,39 @@ class MainActivity : AppCompatActivity() { mSuggestionSearch?.destroy() } + /** + * Migrates historical location data from legacy StringSet format to new JSON format. + * This is a one-time migration to ensure data consistency across app versions. + */ + private fun migrateHistoricalLocationsToJson() { + // Skip if migration has already been performed + if (sharedPrefs.contains("jsonLocations")) { + return + } + + // Get legacy data and migrate + val oldLocations = sharedPrefs.getStringSet("locations", emptySet()) ?: emptySet() + if (oldLocations.isNotEmpty()) { + val locations = oldLocations.mapNotNull { + try { + moe.fuqiuluo.portal.ui.mock.HistoricalLocation.fromString(it) + } catch (e: Exception) { + null + } + } + + // Save as JSON format if locations were successfully parsed + if (locations.isNotEmpty()) { + sharedPrefs.edit() + .putString("jsonLocations", com.alibaba.fastjson2.JSON.toJSONString(locations)) + .apply() + + // Optional: Remove old data after migration + // sharedPrefs.edit().remove("locations").apply() + } + } + } + companion object { private const val REQUEST_PERMISSIONS_CODE = 111 diff --git a/app/src/main/java/moe/fuqiuluo/portal/ext/Perfs.kt b/app/src/main/java/moe/fuqiuluo/portal/ext/Perfs.kt index cb786bd..7f0d185 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/ext/Perfs.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/ext/Perfs.kt @@ -3,6 +3,7 @@ package moe.fuqiuluo.portal.ext import android.content.Context import androidx.core.content.edit import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONArray import com.baidu.mapapi.map.BaiduMap import moe.fuqiuluo.portal.service.MockServiceHelper import moe.fuqiuluo.portal.ui.mock.HistoricalLocation @@ -39,13 +40,51 @@ var Context.selectRoute: HistoricalRoute? putString("selectedRoute", JSON.toJSONString(value)) } +// Get historical locations with JSON format migration val Context.historicalLocations: List get() { - return sharedPrefs.getStringSet("locations", emptySet())?.map { - HistoricalLocation.fromString(it) - } ?: emptyList() + // Check if JSON format is already in use + val jsonLocations = sharedPrefs.getString("jsonLocations", null) + + if (jsonLocations != null) { + try { + return JSON.parseArray(jsonLocations, HistoricalLocation::class.java) + } catch (e: Exception) { + return emptyList() + } + } + + // If no JSON data, try to migrate from old StringSet format + val oldLocations = rawHistoricalLocations + if (oldLocations.isNotEmpty()) { + val locations = oldLocations.mapNotNull { + try { + HistoricalLocation.fromString(it) + } catch (e: Exception) { + null + } + } + + // Save migrated data to JSON format + if (locations.isNotEmpty()) { + jsonHistoricalLocations = locations + } + + return locations + } + + return emptyList() + } + +// New setter using JSON array format +var Context.jsonHistoricalLocations: List + get() = historicalLocations + set(value) = sharedPrefs.edit { + putString("jsonLocations", JSON.toJSONString(value)) + remove("locations") } +// Legacy storage format for backward compatibility var Context.rawHistoricalLocations: Set get() { return sharedPrefs.getStringSet("locations", emptySet()) ?: emptySet() @@ -131,20 +170,6 @@ var Context.hookSensor: Boolean putBoolean("hookSensor", value) } -//var Context.updateInterval: Long -// get() = sharedPrefs.getLong("updateInterval", FakeLoc.updateInterval) -// -// set(value) = sharedPrefs.edit { -// putLong("updateInterval", value) -// } -// -//var Context.hideMock: Boolean -// get() = sharedPrefs.getBoolean("hideMock", FakeLoc.hideMock) -// -// set(value) = sharedPrefs.edit { -// putBoolean("hideMock", value) -// } - var Context.debug: Boolean get() = sharedPrefs.getBoolean("debug", FakeLoc.enableDebugLog) set(value) = sharedPrefs.edit { @@ -173,9 +198,6 @@ var Context.disableFusedProvider: Boolean FakeLoc.disableFusedLocation = value } -/** - * 是否允许地理围栏请求 - */ var Context.enableRequestGeofence: Boolean get() = sharedPrefs.getBoolean("enableRequestGeofence", !FakeLoc.disableRequestGeofence) set(value) = sharedPrefs.edit { @@ -183,9 +205,6 @@ var Context.enableRequestGeofence: Boolean FakeLoc.disableRequestGeofence = !value } -/** - * 是否允许位置获取 - */ var Context.enableGetFromLocation: Boolean get() = sharedPrefs.getBoolean("enableGetFromLocation", !FakeLoc.disableGetFromLocation) set(value) = sharedPrefs.edit { @@ -193,9 +212,6 @@ var Context.enableGetFromLocation: Boolean FakeLoc.disableGetFromLocation = !value } -/** - * 是否允许AGPS模块 - */ var Context.enableAGPS: Boolean get() = sharedPrefs.getBoolean("enableAGPS", FakeLoc.enableAGPS) set(value) = sharedPrefs.edit { @@ -203,9 +219,6 @@ var Context.enableAGPS: Boolean FakeLoc.enableAGPS = value } -/** - * 是否允许NMEA模块 - */ var Context.enableNMEA: Boolean get() = sharedPrefs.getBoolean("enableNMEA", FakeLoc.enableNMEA) set(value) = sharedPrefs.edit { diff --git a/app/src/main/java/moe/fuqiuluo/portal/ui/home/HomeFragment.kt b/app/src/main/java/moe/fuqiuluo/portal/ui/home/HomeFragment.kt index 2483759..be5f644 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/ui/home/HomeFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/ui/home/HomeFragment.kt @@ -43,10 +43,13 @@ import moe.fuqiuluo.portal.bdmap.locateMe import moe.fuqiuluo.portal.bdmap.setMapConfig import moe.fuqiuluo.portal.databinding.FragmentHomeBinding import moe.fuqiuluo.portal.ext.gcj02 +import moe.fuqiuluo.portal.ext.historicalLocations +import moe.fuqiuluo.portal.ext.jsonHistoricalLocations import moe.fuqiuluo.portal.ext.mapType import moe.fuqiuluo.portal.ext.rawHistoricalLocations import moe.fuqiuluo.portal.ext.selectRoute import moe.fuqiuluo.portal.ext.wgs84 +import moe.fuqiuluo.portal.ui.mock.HistoricalLocation import moe.fuqiuluo.portal.ui.viewmodel.BaiduMapViewModel import moe.fuqiuluo.portal.ui.viewmodel.HomeViewModel import java.math.BigDecimal @@ -411,30 +414,27 @@ class HomeFragment : Fragment() { return@setPositiveButton } - fun MutableSet.addLocation( - name: String, - address: String, - lat: Double, - lon: Double - ): Boolean { - if (any { it.split(",")[0] == name }) { - return false - } - add( - "$name,$address,${ - BigDecimal.valueOf(lat).toPlainString() - },${BigDecimal.valueOf(lon).toPlainString()}" - ) - return true - } - with(requireContext()) { - val locations = rawHistoricalLocations.toMutableSet() + val currentLocations = historicalLocations.toMutableList() + val newLocation = HistoricalLocation(name!!, address, newLat!!, newLon!!) + + // Check for duplicate location names var count = 0 - while (!locations.addLocation(name!!, address, newLat!!, newLon!!)) { - name = "$name(${++count})" + var finalName = name!! + while (currentLocations.any { it.name == finalName }) { + finalName = "$name(${++count})" + } + + // Create a new location with updated name if needed + val finalLocation = if (finalName != name) { + newLocation.copy(name = finalName) + } else { + newLocation } - rawHistoricalLocations = locations + + // Add to list and save using JSON format + currentLocations.add(finalLocation) + jsonHistoricalLocations = currentLocations } Toast.makeText(requireContext(), "位置已保存", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocation.kt b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocation.kt index 4d465a6..8adf621 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocation.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocation.kt @@ -1,6 +1,7 @@ package moe.fuqiuluo.portal.ui.mock -import java.math.BigDecimal +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONObject data class HistoricalLocation( val name: String, @@ -9,64 +10,52 @@ data class HistoricalLocation( val lon: Double ) { companion object { - // Format: "name","address","lat","lon" fun fromString(str: String): HistoricalLocation { - // CSV parser supporting commas inside quoted fields + // Try to parse as JSON + return try { + JSON.parseObject(str, HistoricalLocation::class.java) + } catch (e: Exception) { + // Fallback to legacy CSV format for backward compatibility + parseFromLegacyCsv(str) + } + } + + // Legacy CSV parser for backward compatibility + private fun parseFromLegacyCsv(str: String): HistoricalLocation { val fields = mutableListOf() var currentField = StringBuilder() var inQuotes = false - var i = 0 while (i < str.length) { val char = str[i] when { - char == '"' && (i + 1 >= str.length || str[i + 1] != '"') -> { - // Toggle quote state - inQuotes = !inQuotes - } + char == '"' && (i + 1 >= str.length || str[i + 1] != '"') -> inQuotes = !inQuotes char == '"' && i + 1 < str.length && str[i + 1] == '"' -> { - // Handle escaped quotes ("") - currentField.append('"') - // Skip next quote - i++ + currentField.append('"'); i++ } char == ',' && !inQuotes -> { - // Comma as separator fields.add(currentField.toString().trim()) currentField = StringBuilder() } - else -> { - // Regular character - currentField.append(char) - } + else -> currentField.append(char) } i++ } - - // Add the last field fields.add(currentField.toString().trim()) - - if (fields.size != 4) { - throw IllegalArgumentException("Invalid format. Expected 4 fields but got ${fields.size}: $str") - } - - return HistoricalLocation( - name = fields[0].trim('"'), - address = fields[1].trim('"'), - lat = fields[2].trim('"').toDouble(), - lon = fields[3].trim('"').toDouble() - ) + + if (fields.size < 4) throw IllegalArgumentException("Invalid CSV: $str") + + val latStr = fields[fields.size - 2].trim('"') + val lonStr = fields[fields.size - 1].trim('"') + val name = fields.first().trim('"') + val address = fields.subList(1, fields.size - 2) + .joinToString(",") { it.trim('"') } + + return HistoricalLocation(name, address, latStr.toDouble(), lonStr.toDouble()) } } override fun toString(): String { - val plainLat = BigDecimal(lat).toPlainString() - val plainLon = BigDecimal(lon).toPlainString() - - // Quote fields containing commas - val quotedName = if (name.contains(",")) "\"$name\"" else name - val quotedAddress = if (address.contains(",")) "\"$address\"" else address - - return "$quotedName,$quotedAddress,$plainLat,$plainLon" + return JSON.toJSONString(this) } } diff --git a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocationAdapter.kt b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocationAdapter.kt index f02a167..9cf9c64 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocationAdapter.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocationAdapter.kt @@ -107,4 +107,28 @@ class HistoricalLocationAdapter( true } } + + // Get all location items in the adapter + fun getAllItems(): List { + return dataSet.toList() + } + + // Reset adapter data with new locations + fun resetData(newData: List) { + dataSet.clear() + dataSet.addAll(newData) + notifyDataSetChanged() + } + + // Update existing location or add new one + fun updateOrAddItem(location: HistoricalLocation) { + val index = dataSet.indexOfFirst { it.name == location.name } + if (index >= 0) { + dataSet[index] = location + notifyItemChanged(index) + } else { + dataSet.add(location) + notifyItemInserted(dataSet.size - 1) + } + } } \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/MockFragment.kt b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/MockFragment.kt index 7309ad0..c7261aa 100644 --- a/app/src/main/java/moe/fuqiuluo/portal/ui/mock/MockFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/portal/ui/mock/MockFragment.kt @@ -30,6 +30,7 @@ import moe.fuqiuluo.portal.ext.altitude import moe.fuqiuluo.portal.ext.drawOverOtherAppsEnabled import moe.fuqiuluo.portal.ext.historicalLocations import moe.fuqiuluo.portal.ext.hookSensor +import moe.fuqiuluo.portal.ext.jsonHistoricalLocations import moe.fuqiuluo.portal.ext.needOpenSELinux import moe.fuqiuluo.portal.ext.rawHistoricalLocations import moe.fuqiuluo.portal.ext.selectLocation @@ -196,9 +197,10 @@ class MockFragment : Fragment() { .setMessage("确定要删除位置(${location.name})吗?") .setPositiveButton("删除") { _, _ -> historicalLocationAdapter.removeItem(position) - rawHistoricalLocations = rawHistoricalLocations.toMutableSet().apply { - removeIf { it.split(",")[0] == location.name } - } + // Use JSON storage to handle location deletion + val currentLocations = historicalLocations.toMutableList() + currentLocations.removeIf { it.name == location.name && it.lat == location.lat && it.lon == location.lon } + jsonHistoricalLocations = currentLocations showToast("已删除位置") } .setNegativeButton("取消", { _, _ ->