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

Commit 9bf7843b authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

chore(search): remove old search parts

parent 3ee82898
Loading
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -20,11 +20,8 @@ package foundation.e.apps.data

import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Home
import foundation.e.apps.data.application.search.SearchSuggestion

interface StoreRepository {
    suspend fun getHomeScreenData(list: MutableList<Home>): List<Home>
    suspend fun getAppDetails(packageName: String): Application
    suspend fun getSearchResults(pattern: String): List<Application>
    suspend fun getSearchSuggestions(pattern: String): List<SearchSuggestion>
}
+0 −40
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 * Copyright MURENA SAS 2023
 *
 * 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.data.application.search

import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.data.Application

typealias SearchResult = ResultSupreme<Pair<List<Application>, Boolean>>

interface SearchRepository {

    /**
     * Fetches search results from CleanAPK.
     *
     * @return ResultSupreme containing a Pair<List<Application>, Boolean>,
     * where List<Application> contains search results and [Boolean] indicates more data to load or not.
     */
    suspend fun getOpenSourceSearchResults(query: String): SearchResult

    suspend fun getPlayStoreSearchResults(query: String): SearchResult

    suspend fun getSearchSuggestions(query: String): List<SearchSuggestion>
}
+0 −284
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024-2025 e Foundation
 * Copyright (C) 2024 MURENA SAS
 *
 * 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.data.application.search

import foundation.e.apps.data.EnabledSourceState
import foundation.e.apps.data.EnabledStoreRepositoryProvider
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.handleNetworkResult
import foundation.e.apps.data.login.exceptions.GPlayIOException
import foundation.e.apps.data.playstore.PlayStoreRepository
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SearchRepositoryImpl @Inject constructor(
    private val enabledSourceState: EnabledSourceState,
    private val enabledStoreRepositoryProvider: EnabledStoreRepositoryProvider,
    private val applicationDataManager: ApplicationDataManager
) : SearchRepository {

    override suspend fun getOpenSourceSearchResults(query: String): SearchResult {
        val isOpenSourceEnabled = enabledSourceState.awaitIsSourceEnabled(Source.OPEN_SOURCE)
        val isPwaEnabled = enabledSourceState.awaitIsSourceEnabled(Source.PWA)
        val isPlayStoreEnabled = enabledSourceState.awaitIsSourceEnabled(Source.PLAY_STORE)
        val searchResultsByPackageName = if (isPackageName(query)) {
            searchAppsByPackageName(query).data?.first.orEmpty()
        } else {
            emptyList()
        }

        val searchResultsByKeyword = buildList {
            if (isOpenSourceEnabled) {
                addAll(fetchOpenSourceSearchResult(query))
            }
            if (isPwaEnabled) {
                addAll(fetchPwaSearchResult(query))
            }
        }

        return buildFinalSearchResult(
            keywordSpecificResults = searchResultsByKeyword,
            packageSpecificResults = searchResultsByPackageName,
            isPlayStoreEnabled = isPlayStoreEnabled,
        )
    }

    /**
     * Builds the final search result, combining keyword and package-specific results.
     */
    private fun buildFinalSearchResult(
        keywordSpecificResults: List<Application>,
        packageSpecificResults: List<Application>,
        isPlayStoreEnabled: Boolean,
    ): SearchResult {
        return ResultSupreme.Success(
            Pair(
                filterWithKeywordSearch(
                    list = keywordSpecificResults,
                    packageSpecificResults = packageSpecificResults,
                    isPlayStoreEnabled = isPlayStoreEnabled,
                ),
                isPlayStoreEnabled, // Add loading indication for Play Store if enabled
            )
        )
    }

    private fun isPackageName(keyword: String): Boolean {
        val packageNameRegex = ".*\\..*".toRegex() // com.example
        return packageNameRegex.matches(keyword)
    }

    private suspend fun fetchPwaSearchResult(query: String): List<Application> {
        val result = handleNetworkResult {
            enabledStoreRepositoryProvider.awaitStore(Source.PWA)?.getSearchResults(query).orEmpty()
        }

        return if (result.isSuccess()) {
            result.data.orEmpty().map { updatePwa(it) }
        } else {
            emptyList()
        }
    }

    private fun updatePwa(app: Application): Application {
        applicationDataManager.updateStatus(app)
        return app.apply {
            this.source = Source.PWA
            this.updateType()
        }
    }

    private suspend fun fetchOpenSourceSearchResult(query: String): List<Application> {
        val result = handleNetworkResult {
            enabledStoreRepositoryProvider.awaitStore(Source.OPEN_SOURCE)?.getSearchResults(query).orEmpty().map {
                applicationDataManager.updateStatus(it)
                it.updateType()
                it
            }
        }

        return if (result.isSuccess()) {
            result.data.orEmpty()
        } else {
            emptyList()
        }
    }

    private suspend fun searchAppsByPackageName(query: String): SearchResult {
        val apps = mutableListOf<Application?>()
        val isPlayStoreEnabled = enabledSourceState.awaitIsSourceEnabled(Source.PLAY_STORE)
        val isOpenSourceEnabled = enabledSourceState.awaitIsSourceEnabled(Source.OPEN_SOURCE)

        var playStoreApp: Application? = null
        var cleanApkApp: Application? = null

        val result = handleNetworkResult {
            if (isPlayStoreEnabled) {
                playStoreApp = getPlayStoreApp(query)
            }

            if (isOpenSourceEnabled) {
                cleanApkApp = getCleanApkApp(query)
            }
        }

        val resultStatus = result.getResultStatus()
        if (resultStatus != ResultStatus.OK) {
            return ResultSupreme.create(resultStatus, Pair(emptyList(), false))
        }

        // Choose only open source app if it exists both in F-Droid and Play Store.
        // Example: com.fsck.k9
        when {
            cleanApkApp != null -> apps.add(cleanApkApp)
            playStoreApp != null -> apps.add(playStoreApp)
        }

        if (isPlayStoreEnabled) {
            apps.add(Application(isPlaceHolder = true))
        }

        return ResultSupreme.create(resultStatus, Pair(apps.mapNotNull { it }, true))
    }

    /**
     * Filters and combines search results to prevent duplicates while preserving package-specific matches.
     *
     * Package-specific results may contain multiple variants from different sources.
     * These are displayed at the top as-is.
     *
     * Keyword search results are deduplicated by package name and exclude any apps
     * already present in package-specific results.
     */
    private fun filterWithKeywordSearch(
        list: List<Application>,
        packageSpecificResults: List<Application>,
        isPlayStoreEnabled: Boolean,
    ): List<Application> {
        val packageNames = packageSpecificResults.map { it.package_name }.toSet()

        val filteredResults = list.distinctBy { it.package_name }
            .filter { it.package_name !in packageNames }

        val apps = (packageSpecificResults + filteredResults).toMutableList()
        apps.removeIf { it.isPlaceHolder }

        if (isPlayStoreEnabled) {
            apps.add(Application(isPlaceHolder = true))
        }

        return apps.toList()
    }

    private suspend fun getCleanApkApp(query: String): Application? {
        getCleanApkSearchResult(query).let {
            if (it.isSuccess() && it.data!!.package_name.isNotBlank()) {
                return it.data!!
            }
        }

        return null
    }

    private suspend fun getPlayStoreApp(query: String): Application? {
        val storeRepository =
            enabledStoreRepositoryProvider.awaitStore(Source.PLAY_STORE) as? PlayStoreRepository
        return storeRepository?.getAppDetailsWeb(query)
    }

    /*
     * Method to search cleanapk based on package name.
     * This is to be only used for showing an entry in search results list.
     * DO NOT use this to show info on ApplicationFragment as it will not have all the required
     * information to show for an app.
     *
     */
    private suspend fun getCleanApkSearchResult(packageName: String): ResultSupreme<Application> {
        var application = Application()
        val result = handleNetworkResult {
            val results =
                enabledStoreRepositoryProvider.awaitStore(Source.OPEN_SOURCE)
                    ?.getSearchResults(packageName)
                    .orEmpty()

            if (results.isNotEmpty() && results.size == 1) {
                application = results[0]
            }
        }

        return ResultSupreme.create(
            result.getResultStatus(),
            application,
            result.message,
            result.exception,
        )
    }

    override suspend fun getSearchSuggestions(query: String): List<SearchSuggestion> {
        var searchSuggestions = listOf<SearchSuggestion>()
        handleNetworkResult {
            searchSuggestions = enabledStoreRepositoryProvider.awaitEnabledStores()
                .map { it.value }
                .flatMap { storeRepository ->
                    storeRepository.getSearchSuggestions(query)
                }
        }

        return searchSuggestions
    }

    override suspend fun getPlayStoreSearchResults(query: String): SearchResult {
        val source = Source.PLAY_STORE
        val isPlayStoreEnabled = enabledSourceState.awaitIsSourceEnabled(source)

        val result = handleNetworkResult {
            if (!isPlayStoreEnabled) {
                return@handleNetworkResult Pair(
                    listOf(),
                    false
                )
            }

            val searchResults = enabledStoreRepositoryProvider.awaitStore(source)?.getSearchResults(query)
                ?: error("Couldn't get Store for Source: $source")

            val apps = searchResults.toMutableList()
            if (searchResults.isNotEmpty()) {
                apps.add(Application(isPlaceHolder = true))
            }

            return@handleNetworkResult Pair(apps.toList(), true)
        }

        return if (result.isSuccess()) {
            ResultSupreme.Success(result.data as Pair<List<Application>, Boolean>)
        } else {
            ResultSupreme.Error(
                message = "",
                exception = GPlayIOException("Unable to reach Google Play API")
            )
        }
    }
}
+0 −18
Original line number Diff line number Diff line
@@ -20,8 +20,6 @@ package foundation.e.apps.data.cleanapk

import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.data.search.Search
import foundation.e.apps.data.cleanapk.repositories.NUMBER_OF_ITEMS
import foundation.e.apps.data.cleanapk.repositories.NUMBER_OF_PAGES
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.system.SystemInfoProvider
import kotlinx.coroutines.Dispatchers
@@ -31,22 +29,6 @@ import javax.inject.Inject
class CleanApkSearchHelper @Inject constructor(
    private val cleanApkRetrofit: CleanApkRetrofit,
) {
    suspend fun getSearchResults(
        keyword: String,
        appSource: String,
        appType: String
    ): List<Application> {
        return withContext(Dispatchers.IO) {
            getSearchResultPage(
                keyword = keyword,
                appSource = appSource,
                appType = appType,
                page = NUMBER_OF_PAGES,
                pageSize = NUMBER_OF_ITEMS,
            ).apps
        }
    }

    suspend fun getSearchResultPage(
        keyword: String,
        appSource: String,
+0 −15
Original line number Diff line number Diff line
@@ -21,10 +21,8 @@ package foundation.e.apps.data.cleanapk.repositories
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Home
import foundation.e.apps.data.application.search.SearchSuggestion
import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher
import foundation.e.apps.data.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.cleanapk.CleanApkSearchHelper
import foundation.e.apps.data.cleanapk.data.categories.Categories
import foundation.e.apps.data.cleanapk.data.download.Download
import foundation.e.apps.data.cleanapk.data.search.Search
@@ -36,7 +34,6 @@ import javax.inject.Inject
class CleanApkAppsRepository @Inject constructor(
    private val cleanApkRetrofit: CleanApkRetrofit,
    private val homeConverter: HomeConverter,
    private val cleanApkSearchHelper: CleanApkSearchHelper
) : CleanApkRepository, CleanApkDownloadInfoFetcher {
    override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
        val response = cleanApkRetrofit.getHomeScreenData(
@@ -103,18 +100,6 @@ class CleanApkAppsRepository @Inject constructor(
        } ?: Application()
    }

    override suspend fun getSearchResults(pattern: String): List<Application> {
        return cleanApkSearchHelper.getSearchResults(
            keyword = pattern,
            appSource = CleanApkRetrofit.APP_SOURCE_FOSS,
            appType = CleanApkRetrofit.APP_TYPE_NATIVE
        )
    }

    override suspend fun getSearchSuggestions(pattern: String): List<SearchSuggestion> {
        return emptyList() // Not supported yet
    }

    override suspend fun getDownloadInfo(idOrPackageName: String, versionCode: Any?): Response<Download> {
        val version = versionCode?.let { it as String }
        return cleanApkRetrofit.getDownloadInfo(
Loading