Skip to content

Commit 868548d

Browse files
authored
Merge pull request #4352 from vector-im/feature/adm/room-filtering
Fixing case sensitive non latin room name filtering
2 parents 2ce4d8d + d344be5 commit 868548d

File tree

17 files changed

+246
-68
lines changed

17 files changed

+246
-68
lines changed

changelog.d/3968.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixing room search needing exact casing for non latin-1 character named rooms

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,41 @@ package org.matrix.android.sdk.api.query
1919
/**
2020
* Basic query language. All these cases are mutually exclusive.
2121
*/
22-
sealed class QueryStringValue {
23-
object NoCondition : QueryStringValue()
24-
object IsNull : QueryStringValue()
25-
object IsNotNull : QueryStringValue()
26-
object IsEmpty : QueryStringValue()
27-
object IsNotEmpty : QueryStringValue()
28-
data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
29-
data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
22+
sealed interface QueryStringValue {
23+
sealed interface ContentQueryStringValue : QueryStringValue {
24+
val string: String
25+
val case: Case
26+
}
27+
28+
object NoCondition : QueryStringValue
29+
object IsNull : QueryStringValue
30+
object IsNotNull : QueryStringValue
31+
object IsEmpty : QueryStringValue
32+
object IsNotEmpty : QueryStringValue
33+
34+
data class Equals(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
35+
data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
3036

3137
enum class Case {
38+
/**
39+
* Match query sensitive to case
40+
*/
3241
SENSITIVE,
33-
INSENSITIVE
42+
43+
/**
44+
* Match query insensitive to case, this only works for Latin-1 character sets
45+
*/
46+
INSENSITIVE,
47+
48+
/**
49+
* Match query with input normalized (case insensitive)
50+
* Works around Realms inability to sort or filter by case for non Latin-1 character sets
51+
* Expects the target field to contain normalized data
52+
*
53+
* @see org.matrix.android.sdk.internal.util.Normalizer.normalize
54+
*/
55+
NORMALIZED
3456
}
3557
}
58+
59+
internal fun QueryStringValue.isNormalized() = this is QueryStringValue.ContentQueryStringValue && case == QueryStringValue.Case.NORMALIZED

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,24 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
4545
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
4646
import org.matrix.android.sdk.internal.di.MoshiProvider
4747
import org.matrix.android.sdk.internal.query.process
48+
import org.matrix.android.sdk.internal.util.Normalizer
4849
import timber.log.Timber
50+
import javax.inject.Inject
4951

50-
internal object RealmSessionStoreMigration : RealmMigration {
52+
internal class RealmSessionStoreMigration @Inject constructor(
53+
private val normalizer: Normalizer
54+
) : RealmMigration {
5155

52-
const val SESSION_STORE_SCHEMA_VERSION = 18L
56+
companion object {
57+
const val SESSION_STORE_SCHEMA_VERSION = 19L
58+
}
59+
60+
/**
61+
* Forces all RealmSessionStoreMigration instances to be equal
62+
* Avoids Realm throwing when multiple instances of the migration are set
63+
*/
64+
override fun equals(other: Any?) = other is RealmSessionStoreMigration
65+
override fun hashCode() = 1000
5366

5467
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
5568
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@@ -72,6 +85,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
7285
if (oldVersion <= 15) migrateTo16(realm)
7386
if (oldVersion <= 16) migrateTo17(realm)
7487
if (oldVersion <= 17) migrateTo18(realm)
88+
if (oldVersion <= 18) migrateTo19(realm)
7589
}
7690

7791
private fun migrateTo1(realm: DynamicRealm) {
@@ -364,4 +378,16 @@ internal object RealmSessionStoreMigration : RealmMigration {
364378
realm.schema.get("RoomMemberSummaryEntity")
365379
?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
366380
}
381+
382+
private fun migrateTo19(realm: DynamicRealm) {
383+
Timber.d("Step 18 -> 19")
384+
realm.schema.get("RoomSummaryEntity")
385+
?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java)
386+
?.transform {
387+
it.getString(RoomSummaryEntityFields.DISPLAY_NAME)?.let { displayName ->
388+
val normalised = normalizer.normalize(displayName)
389+
it.set(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, normalised)
390+
}
391+
}
392+
}
367393
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ private const val REALM_NAME = "disk_store.realm"
4040
*/
4141
internal class SessionRealmConfigurationFactory @Inject constructor(
4242
private val realmKeysUtils: RealmKeysUtils,
43+
private val realmSessionStoreMigration: RealmSessionStoreMigration,
4344
@SessionFilesDirectory val directory: File,
4445
@SessionId val sessionId: String,
4546
@UserMd5 val userMd5: String,
@@ -71,7 +72,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
7172
.allowWritesOnUiThread(true)
7273
.modules(SessionRealmModule())
7374
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
74-
.migration(RealmSessionStoreMigration)
75+
.migration(realmSessionStoreMigration)
7576
.build()
7677

7778
// Try creating a realm instance and if it succeeds we can clear the flag

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
4242

4343
return RoomSummary(
4444
roomId = roomSummaryEntity.roomId,
45-
displayName = roomSummaryEntity.displayName ?: "",
45+
displayName = roomSummaryEntity.displayName() ?: "",
4646
name = roomSummaryEntity.name ?: "",
4747
topic = roomSummaryEntity.topic ?: "",
4848
avatarUrl = roomSummaryEntity.avatarUrl ?: "",

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
2828
import org.matrix.android.sdk.api.session.room.model.VersioningState
2929
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
3030
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
31+
import org.matrix.android.sdk.internal.session.room.membership.RoomName
3132

3233
internal open class RoomSummaryEntity(
3334
@PrimaryKey var roomId: String = "",
@@ -36,10 +37,24 @@ internal open class RoomSummaryEntity(
3637
var children: RealmList<SpaceChildSummaryEntity> = RealmList()
3738
) : RealmObject() {
3839

39-
var displayName: String? = ""
40-
set(value) {
41-
if (value != field) field = value
40+
private var displayName: String? = ""
41+
42+
fun displayName() = displayName
43+
44+
fun setDisplayName(roomName: RoomName) {
45+
if (roomName.name != displayName) {
46+
displayName = roomName.name
47+
normalizedDisplayName = roomName.normalizedName
4248
}
49+
}
50+
51+
/**
52+
* Workaround for Realm only supporting Latin-1 character sets when sorting
53+
* or filtering by case
54+
* See https://github.com/realm/realm-core/issues/777
55+
*/
56+
private var normalizedDisplayName: String? = ""
57+
4358
var avatarUrl: String? = ""
4459
set(value) {
4560
if (value != field) field = value
@@ -284,5 +299,6 @@ internal open class RoomSummaryEntity(
284299
roomEncryptionTrustLevelStr = value?.name
285300
}
286301
}
302+
287303
companion object
288304
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,41 @@ import io.realm.Case
2020
import io.realm.RealmObject
2121
import io.realm.RealmQuery
2222
import org.matrix.android.sdk.api.query.QueryStringValue
23-
import timber.log.Timber
23+
import org.matrix.android.sdk.api.query.QueryStringValue.ContentQueryStringValue
24+
import org.matrix.android.sdk.internal.util.Normalizer
25+
import javax.inject.Inject
2426

25-
fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
26-
when (queryStringValue) {
27-
is QueryStringValue.NoCondition -> Timber.v("No condition to process")
28-
is QueryStringValue.IsNotNull -> isNotNull(field)
29-
is QueryStringValue.IsNull -> isNull(field)
30-
is QueryStringValue.IsEmpty -> isEmpty(field)
31-
is QueryStringValue.IsNotEmpty -> isNotEmpty(field)
32-
is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase())
33-
is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase())
27+
class QueryStringValueProcessor @Inject constructor(
28+
private val normalizer: Normalizer
29+
) {
30+
31+
fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
32+
return when (queryStringValue) {
33+
is QueryStringValue.NoCondition -> this
34+
is QueryStringValue.IsNotNull -> isNotNull(field)
35+
is QueryStringValue.IsNull -> isNull(field)
36+
is QueryStringValue.IsEmpty -> isEmpty(field)
37+
is QueryStringValue.IsNotEmpty -> isNotEmpty(field)
38+
is ContentQueryStringValue -> when (queryStringValue) {
39+
is QueryStringValue.Equals -> equalTo(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase())
40+
is QueryStringValue.Contains -> contains(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase())
41+
}
42+
}
43+
}
44+
45+
private fun ContentQueryStringValue.toRealmValue(): String {
46+
return when (case) {
47+
QueryStringValue.Case.NORMALIZED -> normalizer.normalize(string)
48+
QueryStringValue.Case.SENSITIVE,
49+
QueryStringValue.Case.INSENSITIVE -> string
50+
}
3451
}
35-
return this
3652
}
3753

3854
private fun QueryStringValue.Case.toRealmCase(): Case {
3955
return when (this) {
4056
QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
41-
QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE
57+
QueryStringValue.Case.SENSITIVE,
58+
QueryStringValue.Case.NORMALIZED -> Case.SENSITIVE
4259
}
4360
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
3030
import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields
3131
import org.matrix.android.sdk.internal.database.query.where
3232
import org.matrix.android.sdk.internal.di.SessionDatabase
33+
import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
3334
import org.matrix.android.sdk.internal.query.process
3435
import org.matrix.android.sdk.internal.util.fetchCopyMap
3536
import javax.inject.Inject
3637

37-
internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
38-
private val groupFactory: GroupFactory) : GroupService {
38+
internal class DefaultGroupService @Inject constructor(
39+
@SessionDatabase private val monarchy: Monarchy,
40+
private val groupFactory: GroupFactory,
41+
private val queryStringValueProcessor: QueryStringValueProcessor,
42+
) : GroupService {
3943

4044
override fun getGroup(groupId: String): Group? {
4145
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
@@ -67,8 +71,10 @@ internal class DefaultGroupService @Inject constructor(@SessionDatabase private
6771
}
6872

6973
private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery<GroupSummaryEntity> {
70-
return GroupSummaryEntity.where(realm)
71-
.process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
72-
.process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
74+
return with(queryStringValueProcessor) {
75+
GroupSummaryEntity.where(realm)
76+
.process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
77+
.process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
78+
}
7379
}
7480
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
3333
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
3434
import org.matrix.android.sdk.internal.di.SessionDatabase
3535
import org.matrix.android.sdk.internal.di.UserId
36+
import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
3637
import org.matrix.android.sdk.internal.query.process
3738
import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask
3839
import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask
@@ -51,7 +52,8 @@ internal class DefaultMembershipService @AssistedInject constructor(
5152
private val leaveRoomTask: LeaveRoomTask,
5253
private val membershipAdminTask: MembershipAdminTask,
5354
@UserId
54-
private val userId: String
55+
private val userId: String,
56+
private val queryStringValueProcessor: QueryStringValueProcessor
5557
) : MembershipService {
5658

5759
@AssistedFactory
@@ -94,15 +96,17 @@ internal class DefaultMembershipService @AssistedInject constructor(
9496
}
9597

9698
private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery<RoomMemberSummaryEntity> {
97-
return RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
98-
.process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId)
99-
.process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
100-
.process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
101-
.apply {
102-
if (queryParams.excludeSelf) {
103-
notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
99+
return with(queryStringValueProcessor) {
100+
RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
101+
.process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId)
102+
.process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
103+
.process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
104+
.apply {
105+
if (queryParams.excludeSelf) {
106+
notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
107+
}
104108
}
105-
}
109+
}
106110
}
107111

108112
override fun getNumberOfJoinedMembers(): Int {

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.query.getOrNull
3434
import org.matrix.android.sdk.internal.database.query.where
3535
import org.matrix.android.sdk.internal.di.UserId
3636
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
37+
import org.matrix.android.sdk.internal.util.Normalizer
3738
import javax.inject.Inject
3839

3940
/**
@@ -42,6 +43,7 @@ import javax.inject.Inject
4243
internal class RoomDisplayNameResolver @Inject constructor(
4344
matrixConfiguration: MatrixConfiguration,
4445
private val displayNameResolver: DisplayNameResolver,
46+
private val normalizer: Normalizer,
4547
@UserId private val userId: String
4648
) {
4749

@@ -54,7 +56,7 @@ internal class RoomDisplayNameResolver @Inject constructor(
5456
* @param roomId: the roomId to resolve the name of.
5557
* @return the room display name
5658
*/
57-
fun resolve(realm: Realm, roomId: String): String {
59+
fun resolve(realm: Realm, roomId: String): RoomName {
5860
// this algorithm is the one defined in
5961
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
6062
// calculateRoomName(room, userId)
@@ -66,12 +68,12 @@ internal class RoomDisplayNameResolver @Inject constructor(
6668
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
6769
name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name
6870
if (!name.isNullOrEmpty()) {
69-
return name
71+
return name.toRoomName()
7072
}
7173
val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
7274
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
7375
if (!name.isNullOrEmpty()) {
74-
return name
76+
return name.toRoomName()
7577
}
7678

7779
val roomMembers = RoomMemberHelper(realm, roomId)
@@ -152,7 +154,7 @@ internal class RoomDisplayNameResolver @Inject constructor(
152154
}
153155
}
154156
}
155-
return name ?: roomId
157+
return (name ?: roomId).toRoomName()
156158
}
157159

158160
/** See [org.matrix.android.sdk.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */
@@ -165,4 +167,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
165167
"${roomMemberSummary.displayName} (${roomMemberSummary.userId})"
166168
}
167169
}
170+
171+
private fun String.toRoomName() = RoomName(this, normalizedName = normalizer.normalize(this))
168172
}
173+
174+
internal data class RoomName(val name: String, val normalizedName: String)

0 commit comments

Comments
 (0)