mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-17 07:41:53 -04:00
fix(node): make node-list search case-insensitive for non-ASCII names (#6753)
This commit is contained in:
1 parent
19bb682c9c
commit
2df817db7a
6 files changed
+47
-11
No files matched your search
-1
@@ -28,7 +28,6 @@ interface NodeInfoReadDataSource {
|
||||
|
||||
fun getNodesFlow(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
|
||||
-2
@@ -37,7 +37,6 @@ class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) :
|
||||
|
||||
override fun getNodesFlow(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
@@ -46,7 +45,6 @@ class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) :
|
||||
db.nodeInfoDao()
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
hopsAwayMax = hopsAwayMax,
|
||||
lastHeardMin = lastHeardMin,
|
||||
|
||||
+2
-2
@@ -49,6 +49,7 @@ import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.model.matchesSearch
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
@@ -191,12 +192,11 @@ class NodeRepositoryImpl(
|
||||
): Flow<List<Node>> = nodeInfoReadDataSource
|
||||
.getNodesFlow(
|
||||
sort = sort.sqlValue,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
hopsAwayMax = if (onlyDirect) 0 else -1,
|
||||
lastHeardMin = if (onlyOnline) onlineTimeThreshold() else -1,
|
||||
)
|
||||
.mapLatest { list -> list.map { it.toModel() } }
|
||||
.mapLatest { list -> list.map { it.toModel() }.filter { node -> node.matchesSearch(filter) } }
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
|
||||
@@ -279,6 +279,8 @@ interface NodeInfoDao {
|
||||
NodeWithRelations,
|
||||
>
|
||||
|
||||
// Text search (name/id) is applied in Kotlin (NodeRepositoryImpl), not here: SQLite's LIKE/UPPER/LOWER only
|
||||
// case-fold ASCII a-z/A-Z, so a WHERE-clause LIKE can't match e.g. "kolså" against "KOLSÅS" (#6750).
|
||||
@Query(
|
||||
"""
|
||||
WITH OurNode AS (
|
||||
@@ -288,11 +290,6 @@ interface NodeInfoDao {
|
||||
)
|
||||
SELECT * FROM nodes
|
||||
WHERE (:includeUnknown = 1 OR short_name IS NOT NULL)
|
||||
AND (:filter = ''
|
||||
OR (long_name LIKE '%' || :filter || '%'
|
||||
OR short_name LIKE '%' || :filter || '%'
|
||||
OR printf('!%08x', CASE WHEN num < 0 THEN num + 4294967296 ELSE num END) LIKE '%' || :filter || '%'
|
||||
OR CAST(CASE WHEN num < 0 THEN num + 4294967296 ELSE num END AS TEXT) LIKE '%' || :filter || '%'))
|
||||
AND (:lastHeardMin = -1 OR last_heard >= :lastHeardMin)
|
||||
AND (:hopsAwayMax = -1 OR (hops_away <= :hopsAwayMax AND hops_away >= 0) OR num = (SELECT myNodeNum FROM my_node LIMIT 1))
|
||||
ORDER BY CASE
|
||||
@@ -328,7 +325,6 @@ interface NodeInfoDao {
|
||||
@Transaction
|
||||
fun getNodes(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
|
||||
@@ -243,3 +243,21 @@ fun Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
|
||||
Config.DeviceConfig.Role.TRACKER,
|
||||
Config.DeviceConfig.Role.TAK_TRACKER,
|
||||
)
|
||||
|
||||
/** Offset converting a negative [Node.num] into its unsigned 32-bit decimal representation. */
|
||||
private const val UNSIGNED_INT_OFFSET = 4294967296L
|
||||
|
||||
private val Node.unsignedNum: Long
|
||||
get() = num.toLong().let { if (it < 0) it + UNSIGNED_INT_OFFSET else it }
|
||||
|
||||
/**
|
||||
* Matches node search text (long/short name, hex id, decimal id) with Unicode-aware case folding.
|
||||
*
|
||||
* Must run in Kotlin, not SQL: SQLite's LIKE/UPPER/LOWER only case-fold ASCII a-z/A-Z, so a query like "kolså" can
|
||||
* never match a stored name of "KOLSÅS" via a SQL WHERE clause (#6750).
|
||||
*/
|
||||
fun Node.matchesSearch(filter: String): Boolean = filter.isBlank() ||
|
||||
user.long_name.contains(filter, ignoreCase = true) ||
|
||||
user.short_name.contains(filter, ignoreCase = true) ||
|
||||
user.id.contains(filter, ignoreCase = true) ||
|
||||
unsignedNum.toString().contains(filter, ignoreCase = true)
|
||||
@@ -137,6 +137,31 @@ class NodeTest {
|
||||
assertTrue(node.mismatchKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSearch_isCaseInsensitiveForNonAsciiLetters() {
|
||||
val node = Node(num = 1, user = User(long_name = "KOLSÅS", short_name = "KOLS"))
|
||||
|
||||
// #6750: "kols" matched via ASCII-only SQL LIKE folding, but "kolså" did not.
|
||||
assertTrue(node.matchesSearch("kols"))
|
||||
assertTrue(node.matchesSearch("kolså"))
|
||||
assertTrue(node.matchesSearch("KOLSÅS"))
|
||||
assertFalse(node.matchesSearch("nomatch"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSearch_matchesHexAndDecimalNodeId() {
|
||||
val node = Node(num = -1, user = User(id = "!ffffffff"))
|
||||
|
||||
assertTrue(node.matchesSearch("ffffffff"))
|
||||
assertTrue(node.matchesSearch("4294967295"))
|
||||
assertFalse(node.matchesSearch("1234"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSearch_blankFilterMatchesEveryNode() {
|
||||
assertTrue(Node(num = 1).matchesSearch(""))
|
||||
}
|
||||
|
||||
private fun nodeWithPosition(num: Int, latitudeI: Int, longitudeI: Int): Node =
|
||||
Node(num = num, position = Position(latitude_i = latitudeI, longitude_i = longitudeI))
|
||||
}
|
||||
Reference in new issue
Block a user