Skip to content
This repository was archived by the owner on Oct 18, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/src/main/java/moe/fuqiuluo/portal/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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

Expand Down
71 changes: 42 additions & 29 deletions app/src/main/java/moe/fuqiuluo/portal/ext/Perfs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,13 +40,51 @@ var Context.selectRoute: HistoricalRoute?
putString("selectedRoute", JSON.toJSONString(value))
}

// Get historical locations with JSON format migration
val Context.historicalLocations: List<HistoricalLocation>
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<HistoricalLocation>
get() = historicalLocations
set(value) = sharedPrefs.edit {
putString("jsonLocations", JSON.toJSONString(value))
remove("locations")
}

// Legacy storage format for backward compatibility
var Context.rawHistoricalLocations: Set<String>
get() {
return sharedPrefs.getStringSet("locations", emptySet()) ?: emptySet()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -173,39 +198,27 @@ var Context.disableFusedProvider: Boolean
FakeLoc.disableFusedLocation = value
}

/**
* 是否允许地理围栏请求
*/
var Context.enableRequestGeofence: Boolean
get() = sharedPrefs.getBoolean("enableRequestGeofence", !FakeLoc.disableRequestGeofence)
set(value) = sharedPrefs.edit {
putBoolean("enableRequestGeofence", value)
FakeLoc.disableRequestGeofence = !value
}

/**
* 是否允许位置获取
*/
var Context.enableGetFromLocation: Boolean
get() = sharedPrefs.getBoolean("enableGetFromLocation", !FakeLoc.disableGetFromLocation)
set(value) = sharedPrefs.edit {
putBoolean("enableGetFromLocation", value)
FakeLoc.disableGetFromLocation = !value
}

/**
* 是否允许AGPS模块
*/
var Context.enableAGPS: Boolean
get() = sharedPrefs.getBoolean("enableAGPS", FakeLoc.enableAGPS)
set(value) = sharedPrefs.edit {
putBoolean("enableAGPS", value)
FakeLoc.enableAGPS = value
}

/**
* 是否允许NMEA模块
*/
var Context.enableNMEA: Boolean
get() = sharedPrefs.getBoolean("enableNMEA", FakeLoc.enableNMEA)
set(value) = sharedPrefs.edit {
Expand Down
42 changes: 21 additions & 21 deletions app/src/main/java/moe/fuqiuluo/portal/ui/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -411,30 +414,27 @@ class HomeFragment : Fragment() {
return@setPositiveButton
}

fun MutableSet<String>.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()
Expand Down
65 changes: 27 additions & 38 deletions app/src/main/java/moe/fuqiuluo/portal/ui/mock/HistoricalLocation.kt
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<String>()
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,28 @@ class HistoricalLocationAdapter(
true
}
}

// Get all location items in the adapter
fun getAllItems(): List<HistoricalLocation> {
return dataSet.toList()
}

// Reset adapter data with new locations
fun resetData(newData: List<HistoricalLocation>) {
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)
}
}
}
8 changes: 5 additions & 3 deletions app/src/main/java/moe/fuqiuluo/portal/ui/mock/MockFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("取消", { _, _ ->
Expand Down
Loading