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

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

Merge branch '4815-send_arch_info_for_cleanApk_apis' into 'main'

feat: support passing device's architecture to CleanAPK API

See merge request !622
parents dbf5b149 cae9d661
Loading
Loading
Loading
Loading
Loading
+28 −19
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ interface CleanApkRetrofit {
    suspend fun getHomeScreenData(
        @Query("type") type: String = APP_TYPE_ANY,
        @Query("source") source: String = APP_SOURCE_ANY,
        @Query("architecture") architecture: String? = null,
    ): Response<HomeScreenResponse>

    // TODO: Reminder that this function is for search App and PWA both
@@ -55,9 +56,11 @@ interface CleanApkRetrofit {
    suspend fun getAppOrPWADetailsByID(
        @Query("id") id: String,
        @Query("architectures") architectures: List<String>? = null,
        @Query("type") type: String? = null
        @Query("type") type: String? = null,
    ): Response<CleanApkApplication>

    @Suppress("LongParameterList")
    // Retrofit endpoint mirrors CleanAPK query parameters; keeping them explicit preserves the API contract
    @GET("apps?action=search")
    suspend fun searchApps(
        @Query("keyword") keyword: String,
@@ -66,8 +69,11 @@ interface CleanApkRetrofit {
        @Query("nres") nres: Int = 20,
        @Query("page") page: Int = 1,
        @Query("by") by: String? = null,
        @Query("architectures") architectures: List<String>? = null,
    ): Response<Search>

    @Suppress("LongParameterList")
    // Endpoint requires explicit query parts; grouping them would hide required inputs
    @GET("apps?action=list_apps")
    suspend fun listApps(
        @Query("category") category: String,
@@ -75,12 +81,14 @@ interface CleanApkRetrofit {
        @Query("type") type: String = APP_TYPE_ANY,
        @Query("nres") nres: Int = 20,
        @Query("page") page: Int = 1,
        @Query("architectures") architectures: List<String>? = null,
    ): Response<Search>

    @GET("apps?action=list_apps")
    suspend fun checkAvailablePackages(
        @Query("package_names[]") packages: List<String>,
        @Query("source") source: String = "open",
        @Query("architectures") architectures: List<String>? = null,
    ): Response<Search>

    @GET("apps?action=download")
@@ -94,5 +102,6 @@ interface CleanApkRetrofit {
    suspend fun getCategoriesList(
        @Query("type") type: String = APP_TYPE_ANY,
        @Query("source") source: String = APP_SOURCE_ANY,
        @Query("architectures") architectures: List<String>? = null,
    ): Response<Categories>
}
+7 −5
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import foundation.e.apps.data.application.data.Application
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.utils.SystemInfoProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
@@ -36,11 +37,12 @@ class CleanApkSearchHelper @Inject constructor(
    ): List<Application> {
        return withContext(Dispatchers.IO) {
            val searchResult = cleanApkRetrofit.searchApps(
                keyword,
                appSource,
                appType,
                NUMBER_OF_ITEMS,
                NUMBER_OF_PAGES
                keyword = keyword,
                source = appSource,
                type = appType,
                nres = NUMBER_OF_ITEMS,
                page = NUMBER_OF_PAGES,
                architectures = SystemInfoProvider.getSupportedArchitectureList(),
            )
            searchResult.body()?.apps.orEmpty()
                .map { it.apply { source = mapSource(it) } }
+26 −7
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ 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
import foundation.e.apps.data.enums.Source
import foundation.e.apps.utils.SystemInfoProvider
import retrofit2.Response
import javax.inject.Inject

@@ -40,7 +41,8 @@ class CleanApkAppsRepository @Inject constructor(
    override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
        val response = cleanApkRetrofit.getHomeScreenData(
            CleanApkRetrofit.APP_TYPE_ANY,
            CleanApkRetrofit.APP_SOURCE_FOSS
            CleanApkRetrofit.APP_SOURCE_FOSS,
            SystemInfoProvider.getSupportedArchitecture(),
        )

        val home = response.body()?.home ?: throw IllegalStateException("No home data found")
@@ -67,25 +69,38 @@ class CleanApkAppsRepository @Inject constructor(
            CleanApkRetrofit.APP_SOURCE_FOSS,
            CleanApkRetrofit.APP_TYPE_ANY,
            NUMBER_OF_ITEMS,
            NUMBER_OF_PAGES
            NUMBER_OF_PAGES,
            SystemInfoProvider.getSupportedArchitectureList()
        )
    }

    override suspend fun getCategories(): Response<Categories> {
        return cleanApkRetrofit.getCategoriesList(
            CleanApkRetrofit.APP_TYPE_ANY,
            CleanApkRetrofit.APP_SOURCE_FOSS
            CleanApkRetrofit.APP_SOURCE_FOSS,
            SystemInfoProvider.getSupportedArchitectureList()
        )
    }

    override suspend fun checkAvailablePackages(packageNames: List<String>): Response<Search> {
        return cleanApkRetrofit.checkAvailablePackages(packageNames)
        return cleanApkRetrofit.checkAvailablePackages(
            packages = packageNames,
            architectures = SystemInfoProvider.getSupportedArchitectureList()
        )
    }

    override suspend fun getAppDetails(packageName: String): Application {
        val apps = cleanApkRetrofit.checkAvailablePackages(listOf(packageName))
        val apps = cleanApkRetrofit.checkAvailablePackages(
            packages = listOf(packageName),
            architectures = SystemInfoProvider.getSupportedArchitectureList()
        )
        val app = apps.body()?.apps?.firstOrNull() ?: return Application()
        val response = cleanApkRetrofit.getAppOrPWADetailsByID(app._id, null, null)
        val response = cleanApkRetrofit.getAppOrPWADetailsByID(
            id = app._id,
            architectures = SystemInfoProvider.getSupportedArchitectureList(),
            type = null
        )

        return response.body()?.app ?: return Application()
    }

@@ -103,6 +118,10 @@ class CleanApkAppsRepository @Inject constructor(

    override suspend fun getDownloadInfo(idOrPackageName: String, versionCode: Any?): Response<Download> {
        val version = versionCode?.let { it as String }
        return cleanApkRetrofit.getDownloadInfo(idOrPackageName, version, null)
        return cleanApkRetrofit.getDownloadInfo(
            id = idOrPackageName,
            version = version,
            architecture = SystemInfoProvider.getSupportedArchitecture()
        )
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -51,4 +51,12 @@ object SystemInfoProvider {
        }
        return descriptionJson.toString()
    }

    fun getSupportedArchitecture(): String? {
        return getSupportedArchitectureList().firstOrNull()
    }

    fun getSupportedArchitectureList(): List<String> {
        return Build.SUPPORTED_ABIS.filterNot { it.isNullOrBlank() }
    }
}
+111 −20
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/>.
 *
 */

/*
 * Tests for CleanApkSearchHelper ensuring query parameters and source mapping.
 */

package foundation.e.apps.data.cleanapk

import com.google.common.truth.Truth.assertThat
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.utils.SystemInfoProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import retrofit2.Response

@OptIn(ExperimentalCoroutinesApi::class)
class CleanApkSearchHelperTest {

    private val retrofit: CleanApkRetrofit = mockk()
    private val cleanApkRetrofit: CleanApkRetrofit = mockk(relaxed = true)
    private lateinit var helper: CleanApkSearchHelper

    @Before
    fun setUp() {
        helper = CleanApkSearchHelper(retrofit)
        mockkObject(SystemInfoProvider)
        helper = CleanApkSearchHelper(cleanApkRetrofit)
    }

    @After
    fun tearDown() {
        unmockkObject(SystemInfoProvider)
    }

    @Test
    fun getSearchResultsMapsPwaAndOpenSource() = runTest {
        val pwa = Application(package_name = "pwa.app", is_pwa = true)
        val oss = Application(package_name = "oss.app", is_pwa = false)
        val search = Search(apps = listOf(pwa, oss), success = true)
        coEvery { retrofit.searchApps(any(), any(), any(), any(), any()) } returns Response.success(search)
    fun `mapSource returns PWA when app is pwa`() {
        val result = invokeMapSource(Application(is_pwa = true))
        assertThat(result).isEqualTo(Source.PWA)
    }

        val results = helper.getSearchResults("k", CleanApkRetrofit.APP_SOURCE_ANY, CleanApkRetrofit.APP_TYPE_ANY)
    @Test
    fun `mapSource returns OPEN_SOURCE when app is native`() {
        val result = invokeMapSource(Application(is_pwa = false))
        assertThat(result).isEqualTo(Source.OPEN_SOURCE)
    }

        val mappedPwa = results.first { it.package_name == "pwa.app" }
        val mappedOss = results.first { it.package_name == "oss.app" }
        assertThat(mappedPwa.source).isEqualTo(Source.PWA)
        assertThat(mappedOss.source).isEqualTo(Source.OPEN_SOURCE)
    @Test
    fun `getSearchResults maps PWA and native sources and forwards architectures`() = runTest {
        val architectures = listOf("arm64-v8a", "armeabi-v7a")
        every { SystemInfoProvider.getSupportedArchitectureList() } returns architectures
        val pwaApp = Application(_id = "pwa", is_pwa = true)
        val nativeApp = Application(_id = "native", is_pwa = false)

        coEvery {
            cleanApkRetrofit.searchApps(any(), any(), any(), any(), any(), any(), any())
        } returns Response.success(
            Search(apps = listOf(pwaApp, nativeApp))
        )

        val result = helper.getSearchResults(
            keyword = "signal",
            appSource = CleanApkRetrofit.APP_SOURCE_FOSS,
            appType = CleanApkRetrofit.APP_TYPE_ANY
        )

        coVerify {
            cleanApkRetrofit.searchApps(
                keyword = "signal",
                source = CleanApkRetrofit.APP_SOURCE_FOSS,
                type = CleanApkRetrofit.APP_TYPE_ANY,
                nres = NUMBER_OF_ITEMS,
                page = NUMBER_OF_PAGES,
                by = null,
                architectures = architectures
            )
        }
        assertThat(result).hasSize(2)
        assertThat(result.first { it._id == "pwa" }.source).isEqualTo(Source.PWA)
        assertThat(result.first { it._id == "native" }.source).isEqualTo(Source.OPEN_SOURCE)
    }

    @Test
    fun getSearchResultsReturnsEmptyWhenBodyNull() = runTest {
        coEvery { retrofit.searchApps(any(), any(), any(), any(), any()) } returns Response.success(null)
    fun `getSearchResults returns empty list when response body missing`() = runTest {
        every { SystemInfoProvider.getSupportedArchitectureList() } returns emptyList()
        coEvery {
            cleanApkRetrofit.searchApps(any(), any(), any(), any(), any(), any(), any())
        } returns Response.success(null)

        val results = helper.getSearchResults("k", CleanApkRetrofit.APP_SOURCE_ANY, CleanApkRetrofit.APP_TYPE_ANY)
        val result = helper.getSearchResults(
            keyword = "none",
            appSource = CleanApkRetrofit.APP_SOURCE_FOSS,
            appType = CleanApkRetrofit.APP_TYPE_NATIVE
        )

        assertThat(result).isEmpty()
        coVerify {
            cleanApkRetrofit.searchApps(
                keyword = "none",
                source = CleanApkRetrofit.APP_SOURCE_FOSS,
                type = CleanApkRetrofit.APP_TYPE_NATIVE,
                nres = NUMBER_OF_ITEMS,
                page = NUMBER_OF_PAGES,
                by = null,
                architectures = emptyList()
            )
        }
    }

        assertThat(results).isEmpty()
    private fun invokeMapSource(app: Application): Source {
        val method =
            CleanApkSearchHelper::class.java.getDeclaredMethod("mapSource", Application::class.java)
        method.isAccessible = true
        return method.invoke(helper, app) as Source
    }
}
Loading