Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit b1b8c6b0 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: inline store selection updates in SearchViewModelV2

Removes the dedicated store-selection use case and result type, moving the logic into SearchViewModelV2 so store-change handling stays localized and clears suggestions when Play Store is disabled.

Adds a regression test for suggestion clearing and renames the search submission result file to match its class.
parent 58dcdb9c
Loading
Loading
Loading
Loading
Loading
+0 −8
Original line number Diff line number Diff line
@@ -28,11 +28,3 @@ data class SearchSubmissionResult(
    val nextVersion: Int,
    val searchRequest: SearchRequest?,
)

data class StoreSelectionUpdate(
    val enabledSources: List<Source>,
    val selectedSource: Source?,
    val hasSubmittedSearch: Boolean,
    val suggestionsEnabled: Boolean,
    val searchRequest: SearchRequest?,
)
+0 −63
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.data.Stores
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.preference.AppLoungePreference
import javax.inject.Inject

class UpdateSearchForStoreSelectionUseCase @Inject constructor(
    private val stores: Stores,
    private val appLoungePreference: AppLoungePreference,
) {
    operator fun invoke(
        currentQuery: String,
        selectedSource: Source?,
        hasSubmittedSearch: Boolean,
        currentVersion: Int,
    ): StoreSelectionUpdate {
        val enabledSources = stores.getEnabledSearchSources()
        val resolvedSelectedSource = selectedSource?.takeIf { enabledSources.contains(it) }
            ?: enabledSources.firstOrNull()
        val updatedHasSubmittedSearch = hasSubmittedSearch && enabledSources.isNotEmpty()
        val shouldUpdateRequest = hasSubmittedSearch && currentQuery.isNotBlank()
        val searchRequest = when {
            shouldUpdateRequest && enabledSources.isNotEmpty() -> SearchRequest(
                query = currentQuery,
                enabledSources = enabledSources,
                version = currentVersion,
            )
            !hasSubmittedSearch -> SearchRequest(
                query = "",
                enabledSources = enabledSources,
                version = currentVersion,
            )
            else -> null
        }

        return StoreSelectionUpdate(
            enabledSources = enabledSources,
            selectedSource = resolvedSelectedSource,
            hasSubmittedSearch = updatedHasSubmittedSearch,
            suggestionsEnabled = appLoungePreference.isPlayStoreSelected(),
            searchRequest = searchRequest,
        )
    }
}
+33 −13
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ import foundation.e.apps.domain.search.FetchSearchSuggestionsUseCase
import foundation.e.apps.domain.search.PlayStoreSearchPagingUseCase
import foundation.e.apps.domain.search.PrepareSearchSubmissionUseCase
import foundation.e.apps.domain.search.SearchRequest
import foundation.e.apps.domain.search.UpdateSearchForStoreSelectionUseCase
import foundation.e.apps.install.download.data.DownloadProgress
import foundation.e.apps.ui.compose.state.InstallStatusReconciler
import foundation.e.apps.ui.compose.state.InstallStatusStream
@@ -87,7 +86,6 @@ class SearchViewModelV2 @Inject constructor(
    playStoreSearchPagingUseCase: PlayStoreSearchPagingUseCase,
    private val fetchSearchSuggestionsUseCase: FetchSearchSuggestionsUseCase,
    private val prepareSearchSubmissionUseCase: PrepareSearchSubmissionUseCase,
    private val updateSearchForStoreSelectionUseCase: UpdateSearchForStoreSelectionUseCase,
    private val stores: Stores,
    private val installStatusStream: InstallStatusStream,
    private val installStatusReconciler: InstallStatusReconciler,
@@ -231,23 +229,45 @@ class SearchViewModelV2 @Inject constructor(

    private fun handleStoreSelectionChanged() {
        val currentState = _uiState.value
        val update = updateSearchForStoreSelectionUseCase(
            currentQuery = currentState.query,
            selectedSource = currentState.selectedTab?.toSource(),
            hasSubmittedSearch = currentState.hasSubmittedSearch,
            currentVersion = currentState.searchVersion,
        val enabledSources = stores.getEnabledSearchSources()
        val hasEnabledSources = enabledSources.isNotEmpty()

        val selectedSource = currentState.selectedTab?.toSource()
        val resolvedSelectedSource = selectedSource
            ?.takeIf { enabledSources.contains(it) }
            ?: enabledSources.firstOrNull()

        val updatedHasSubmittedSearch = currentState.hasSubmittedSearch && hasEnabledSources
        val shouldUpdateRequest = currentState.hasSubmittedSearch && currentState.query.isNotBlank()

        val searchRequest = when {
            shouldUpdateRequest && hasEnabledSources -> SearchRequest(
                query = currentState.query,
                enabledSources = enabledSources,
                version = currentState.searchVersion,
            )
            !currentState.hasSubmittedSearch -> SearchRequest(
                query = "",
                enabledSources = enabledSources,
                version = currentState.searchVersion,
            )
            else -> null
        }

        val areSuggestionsEnabled = appLoungePreference.isPlayStoreSelected()

        _uiState.update { current ->
            val updatedSuggestions = if (areSuggestionsEnabled) current.suggestions else emptyList()
            current.copy(
                availableTabs = update.enabledSources.toSearchTabTypes(),
                selectedTab = update.selectedSource?.toSearchTabTypeOrNull(),
                hasSubmittedSearch = update.hasSubmittedSearch,
                isSuggestionVisible = current.isSuggestionVisible && update.suggestionsEnabled,
                availableTabs = enabledSources.toSearchTabTypes(),
                selectedTab = resolvedSelectedSource?.toSearchTabTypeOrNull(),
                hasSubmittedSearch = updatedHasSubmittedSearch,
                suggestions = updatedSuggestions,
                isSuggestionVisible = current.isSuggestionVisible && areSuggestionsEnabled,
            )
        }

        update.searchRequest?.let { request ->
        searchRequest?.let { request ->
            searchRequests.value = request
        }
    }
+0 −106
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import com.google.common.truth.Truth.assertThat
import foundation.e.apps.data.Stores
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.preference.AppLoungePreference
import io.mockk.every
import io.mockk.mockk
import org.junit.Before
import org.junit.Test

class UpdateSearchForStoreSelectionUseCaseTest {

    private lateinit var stores: Stores
    private lateinit var appLoungePreference: AppLoungePreference
    private lateinit var useCase: UpdateSearchForStoreSelectionUseCase

    @Before
    fun setUp() {
        stores = mockk()
        appLoungePreference = mockk()
        useCase = UpdateSearchForStoreSelectionUseCase(
            stores,
            appLoungePreference,
        )
    }

    @Test
    fun `submitted search updates request when enabled sources are available`() {
        every { stores.getEnabledSearchSources() } returns listOf(
            Source.PLAY_STORE,
            Source.PWA,
        )
        every { appLoungePreference.isPlayStoreSelected() } returns true

        val result = useCase(
            currentQuery = "apps",
            selectedSource = Source.PWA,
            hasSubmittedSearch = true,
            currentVersion = 4,
        )

        assertThat(result.searchRequest).isNotNull()
        assertThat(result.searchRequest?.query).isEqualTo("apps")
        assertThat(result.searchRequest?.version).isEqualTo(4)
        assertThat(result.selectedSource).isEqualTo(Source.PWA)
        assertThat(result.hasSubmittedSearch).isTrue()
        assertThat(result.suggestionsEnabled).isTrue()
    }

    @Test
    fun `no submitted search emits empty query request`() {
        every { stores.getEnabledSearchSources() } returns listOf(Source.OPEN_SOURCE)
        every { appLoungePreference.isPlayStoreSelected() } returns false

        val result = useCase(
            currentQuery = "",
            selectedSource = Source.OPEN_SOURCE,
            hasSubmittedSearch = false,
            currentVersion = 2,
        )

        assertThat(result.searchRequest).isNotNull()
        assertThat(result.searchRequest?.query).isEqualTo("")
        assertThat(result.searchRequest?.version).isEqualTo(2)
        assertThat(result.hasSubmittedSearch).isFalse()
        assertThat(result.suggestionsEnabled).isFalse()
        assertThat(result.selectedSource).isEqualTo(Source.OPEN_SOURCE)
    }

    @Test
    fun `empty enabled sources clear selection and skip request update`() {
        every { stores.getEnabledSearchSources() } returns emptyList()
        every { appLoungePreference.isPlayStoreSelected() } returns true

        val result = useCase(
            currentQuery = "apps",
            selectedSource = Source.PLAY_STORE,
            hasSubmittedSearch = true,
            currentVersion = 1,
        )

        assertThat(result.enabledSources).isEmpty()
        assertThat(result.selectedSource).isNull()
        assertThat(result.hasSubmittedSearch).isFalse()
        assertThat(result.searchRequest).isNull()
    }
}
+19 −7
Original line number Diff line number Diff line
@@ -40,7 +40,6 @@ import foundation.e.apps.domain.search.FetchSearchSuggestionsUseCase
import foundation.e.apps.domain.search.PlayStoreAppMapper
import foundation.e.apps.domain.search.PlayStoreSearchPagingUseCase
import foundation.e.apps.domain.search.PrepareSearchSubmissionUseCase
import foundation.e.apps.domain.search.UpdateSearchForStoreSelectionUseCase
import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil
import foundation.e.apps.ui.compose.state.InstallStatusReconciler
import foundation.e.apps.ui.compose.state.InstallStatusStream
@@ -80,7 +79,6 @@ class SearchViewModelV2Test {
    private lateinit var playStoreSearchPagingUseCase: PlayStoreSearchPagingUseCase
    private lateinit var fetchSearchSuggestionsUseCase: FetchSearchSuggestionsUseCase
    private lateinit var prepareSearchSubmissionUseCase: PrepareSearchSubmissionUseCase
    private lateinit var updateSearchForStoreSelectionUseCase: UpdateSearchForStoreSelectionUseCase
    private lateinit var stores: Stores
    private lateinit var installStatusStream: InstallStatusStream
    private lateinit var installStatusReconciler: InstallStatusReconciler
@@ -320,6 +318,25 @@ class SearchViewModelV2Test {
        assertFalse(state.isSuggestionVisible)
    }

    @Test
    fun `store change clears suggestions when play store turns off`() = runTest {
        playStoreSelected = true
        buildViewModel()
        viewModel.onQueryChanged("tel")
        advanceDebounce()

        val stateWithSuggestions = viewModel.uiState.value
        assertTrue(stateWithSuggestions.isSuggestionVisible)
        assertFalse(stateWithSuggestions.suggestions.isEmpty())

        stores.disableStore(Source.PLAY_STORE)
        runStoreUpdates()

        val state = viewModel.uiState.value
        assertFalse(state.isSuggestionVisible)
        assertTrue(state.suggestions.isEmpty())
    }

    @Test
    fun `store change removing all tabs clears submitted state`() = runTest {
        playStoreSelected = true
@@ -621,17 +638,12 @@ class SearchViewModelV2Test {
        stores = buildStores()
        fetchSearchSuggestionsUseCase = FetchSearchSuggestionsUseCase(suggestionSource, preference)
        prepareSearchSubmissionUseCase = PrepareSearchSubmissionUseCase(stores)
        updateSearchForStoreSelectionUseCase = UpdateSearchForStoreSelectionUseCase(
            stores,
            preference,
        )
        viewModel = SearchViewModelV2(
            preference,
            cleanApkSearchPagingUseCase,
            playStoreSearchPagingUseCase,
            fetchSearchSuggestionsUseCase,
            prepareSearchSubmissionUseCase,
            updateSearchForStoreSelectionUseCase,
            stores,
            installStatusStream,
            installStatusReconciler,