mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all (#7249)
* Add a message search abstraction over the Rust SDK's local index Wraps the SDK's search service so the rest of the app never sees MatrixRustSDK types, following the existing convention in libraries/matrix. - MessageSearch, MessageSearchService, MessageSearchResult and MessageSearchPaginationState describe the surface in the api module. - RustMessageSearch and RustMessageSearchService implement it, with MessageSearchResultMapper and MessageSearchResultsProcessor turning SDK results into Kotlin data classes. - TantivyQueryEscaper escapes the query so a user typing ":)" or "AND" searches for that literal text rather than tripping the tantivy parser, and joins every word as a Must clause. - Fakes and unit tests cover the mapping, the processor and the escaper, the last checked against a real tantivy 0.26.1 parser. * Attach the encrypted search index behind a disabled feature flag Gives the client an on-device tantivy index encrypted with the session's client secret, so encrypted rooms can be searched at all. - The index store is attached at ClientBuilder time via withSearchIndexStore, in a search-index directory alongside the session, keyed on the client secret. A session without one gets no index. - MatrixClient exposes isMessageSearchAvailable so callers gate on the real indexing capability rather than on the raw flag, and getCacheSize() counts the index directory so it stays visible in the settings figures. - FeatureFlags.MessageSearch (feature.message_search) is off by default and isFinished = false. It is read once at ClientBuilder time, so develop stays releasable and nothing reaches a user. * Simplify search index wiring following review Tie search availability to the MessageSearch feature flag alone and let the index be created without a client secret, encrypting it only when the session has one, matching the SDK's SQLite stores. Make getBaseClientBuilder take the flag as a required parameter and return ClientBuilder directly again, dropping the BaseClientBuilder wrapper and the mid-restore snapshot parameter. Check the flag through FeatureFlagService in RustMatrixAuthenticationService instead of caching it in a field, and revert the extra matrix_sdk_search tracing target, which belongs in the SDK's defaults.
This commit is contained in:
+4
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
@@ -55,6 +56,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
|
||||
import io.element.android.libraries.matrix.test.pushers.FakePushersService
|
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.search.FakeMessageSearchService
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
@@ -91,6 +93,8 @@ class FakeMatrixClient(
|
||||
override val syncService: SyncService = FakeSyncService(),
|
||||
override val encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
override val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
|
||||
override val isMessageSearchAvailable: Boolean = false,
|
||||
override val messageSearchService: MessageSearchService = FakeMessageSearchService(),
|
||||
override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
||||
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test.search
|
||||
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearch
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchPaginationState
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeMessageSearch(
|
||||
private val setQueryResult: suspend (String) -> Result<Unit> = { Result.success(Unit) },
|
||||
private val paginateResult: suspend () -> Result<Unit> = { Result.success(Unit) },
|
||||
private val controllablePagination: Boolean = false,
|
||||
) : MessageSearch {
|
||||
private val mutableResults = MutableStateFlow<ImmutableList<MessageSearchResult>>(persistentListOf())
|
||||
override val results: StateFlow<ImmutableList<MessageSearchResult>> = mutableResults
|
||||
|
||||
private val mutablePaginationState = MutableStateFlow<MessageSearchPaginationState>(
|
||||
MessageSearchPaginationState.Idle(endReached = false)
|
||||
)
|
||||
override val paginationState: StateFlow<MessageSearchPaginationState> = mutablePaginationState
|
||||
|
||||
var lastQuery: String? = null
|
||||
private set
|
||||
|
||||
var setQueryCallCount = 0
|
||||
private set
|
||||
|
||||
var paginateCallCount = 0
|
||||
private set
|
||||
|
||||
var completedPaginateCallCount = 0
|
||||
private set
|
||||
|
||||
var cancelledPaginateCallCount = 0
|
||||
private set
|
||||
|
||||
private var pendingPagination = CompletableDeferred<PaginationCompletion>()
|
||||
|
||||
override suspend fun setQuery(query: String): Result<Unit> {
|
||||
lastQuery = query
|
||||
setQueryCallCount++
|
||||
return setQueryResult(query)
|
||||
}
|
||||
|
||||
override suspend fun paginate(): Result<Unit> {
|
||||
paginateCallCount++
|
||||
if (!controllablePagination) {
|
||||
return paginateResult()
|
||||
}
|
||||
|
||||
mutablePaginationState.value = MessageSearchPaginationState.Loading
|
||||
return try {
|
||||
val completion = pendingPagination.await()
|
||||
completedPaginateCallCount++
|
||||
mutablePaginationState.value = MessageSearchPaginationState.Idle(endReached = completion.endReached)
|
||||
completion.result
|
||||
} catch (exception: CancellationException) {
|
||||
cancelledPaginateCallCount++
|
||||
mutablePaginationState.value = MessageSearchPaginationState.Idle(endReached = false)
|
||||
throw exception
|
||||
} finally {
|
||||
pendingPagination = CompletableDeferred()
|
||||
}
|
||||
}
|
||||
|
||||
fun emitResults(results: ImmutableList<MessageSearchResult>) {
|
||||
mutableResults.value = results
|
||||
}
|
||||
|
||||
fun emitPaginationState(state: MessageSearchPaginationState) {
|
||||
mutablePaginationState.value = state
|
||||
}
|
||||
|
||||
fun completePagination(
|
||||
result: Result<Unit> = Result.success(Unit),
|
||||
endReached: Boolean = false,
|
||||
) {
|
||||
check(pendingPagination.complete(PaginationCompletion(result, endReached))) {
|
||||
"There is no pending pagination to complete."
|
||||
}
|
||||
}
|
||||
|
||||
private data class PaginationCompletion(
|
||||
val result: Result<Unit>,
|
||||
val endReached: Boolean,
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test.search
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearch
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class FakeMessageSearchService(
|
||||
private val messageSearch: MessageSearch = FakeMessageSearch(),
|
||||
) : MessageSearchService {
|
||||
/** The room the search was scoped to on the last call — null means all rooms. */
|
||||
var lastRoomId: RoomId? = null
|
||||
private set
|
||||
|
||||
var createMessageSearchCallCount = 0
|
||||
private set
|
||||
|
||||
override fun createMessageSearch(scope: CoroutineScope, roomId: RoomId?): MessageSearch {
|
||||
lastRoomId = roomId
|
||||
createMessageSearchCallCount++
|
||||
return messageSearch
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user